From 6a1eb5e2ac8bf6886c801e3c1cbf2e2fad58fab5 Mon Sep 17 00:00:00 2001 From: Aaron Date: Sat, 7 Nov 2026 20:13:20 +0000 Subject: [PATCH] Add x86_64 kernel bring-up with build-selected platform adapters --- .gitignore | 2 + Dockerfile | 6 +- doc/planning/x86_64.md | 353 ++++++++++++ docker-compose.yml | 5 +- mkdocs.yml | 1 + src/arch/common/arch.common.interrupts.pas | 23 + src/arch/common/arch.common.legacyirq.pas | 69 +++ src/arch/common/platform.cpu.inc | 20 + src/arch/common/platform.diagnostics.inc | 18 + src/arch/common/platform.memory.inc | 18 + src/arch/common/platform.process.inc | 15 + src/arch/common/system.types.inc | 197 +++++++ src/arch/x86/arch.x86.console.pas | 82 +++ src/arch/x86/arch.x86.irq.pas | 24 +- src/arch/x86/arch.x86.multiboot.pas | 67 +-- src/arch/x86/arch.x86.startup.pas | 24 + src/arch/x86/isr/arch.x86.isr.tmr0.pas | 7 +- src/arch/x86/platform/arch.platform.cpu.pas | 53 ++ .../platform/arch.platform.diagnostics.pas | 33 ++ .../x86/platform/arch.platform.memory.pas | 81 +++ .../x86/platform/arch.platform.process.pas | 23 + src/arch/x86/proc/arch.x86.proc.context.pas | 35 ++ src/arch/x86/proc/arch.x86.proc.sched.pas | 7 +- src/arch/x86/system.pas | 187 +------ src/arch/x86_64/arch.x86_64.boot.pas | 83 +++ src/arch/x86_64/arch.x86_64.context.pas | 156 ++++++ src/arch/x86_64/arch.x86_64.debug.pas | 39 ++ src/arch/x86_64/arch.x86_64.irq.pas | 91 ++++ src/arch/x86_64/arch.x86_64.isr.pas | 245 +++++++++ src/arch/x86_64/arch.x86_64.util.pas | 67 +++ src/arch/x86_64/boot/stub.asm | 209 +++++++ src/arch/x86_64/isr/entry.asm | 276 ++++++++++ .../memory/arch.x86_64.memory.physical.pas | 304 +++++++++++ .../memory/arch.x86_64.memory.virtual.pas | 310 +++++++++++ .../x86_64/platform/arch.platform.cpu.pas | 35 ++ .../platform/arch.platform.diagnostics.pas | 32 ++ .../x86_64/platform/arch.platform.memory.pas | 38 ++ .../x86_64/platform/arch.platform.process.pas | 23 + .../x86_64/proc/arch.x86_64.proc.sched.pas | 173 ++++++ src/arch/x86_64/system.pas | 50 ++ src/asuro.pas | 2 +- src/boot/boot.multiboot.pas | 75 +++ src/core/core.util.pas | 49 +- src/core/ds/core.ds.dynamic.pas | 183 +++++++ src/core/ds/core.ds.lists.pas | 389 +------------ src/core/ds/core.ds.types.pas | 33 ++ src/core/strings/core.strings.pas | 82 +-- src/debug/debug.early.pas | 55 ++ src/driver/storage/driver.storage.fdtable.pas | 204 +++++-- src/driver/storage/driver.storage.types.pas | 3 +- src/io/io.directory.pas | 31 ++ src/io/io.stdio.pas | 395 +++++++------- src/io/io.types.pas | 53 ++ src/memory/memory.bitmap.pas | 98 ++++ src/memory/memory.heap.pas | 367 +++++-------- src/memory/memory.raw.pas | 50 ++ src/proc/proc.mgr.pas | 510 ++++++++---------- src/proc/proc.scheduler.pas | 67 +++ src/proc/proc.types.pas | 18 +- toolchain/check_arch_boundaries.py | 43 ++ toolchain/compile.sh | 27 +- toolchain/compile_isogen.sh | 11 +- toolchain/compile_link.sh | 13 +- toolchain/compile_sources.sh | 28 +- toolchain/compile_stub.sh | 11 +- toolchain/target.sh | 36 ++ toolchain/x86_64/linker.script | 31 ++ toolchain/x86_64/test_boot.ps1 | 112 ++++ virtualbox-wrapper.ps1 | 26 +- 69 files changed, 4981 insertions(+), 1502 deletions(-) create mode 100644 doc/planning/x86_64.md create mode 100644 src/arch/common/arch.common.interrupts.pas create mode 100644 src/arch/common/arch.common.legacyirq.pas create mode 100644 src/arch/common/platform.cpu.inc create mode 100644 src/arch/common/platform.diagnostics.inc create mode 100644 src/arch/common/platform.memory.inc create mode 100644 src/arch/common/platform.process.inc create mode 100644 src/arch/common/system.types.inc create mode 100644 src/arch/x86/arch.x86.console.pas create mode 100644 src/arch/x86/arch.x86.startup.pas create mode 100644 src/arch/x86/platform/arch.platform.cpu.pas create mode 100644 src/arch/x86/platform/arch.platform.diagnostics.pas create mode 100644 src/arch/x86/platform/arch.platform.memory.pas create mode 100644 src/arch/x86/platform/arch.platform.process.pas create mode 100644 src/arch/x86/proc/arch.x86.proc.context.pas create mode 100644 src/arch/x86_64/arch.x86_64.boot.pas create mode 100644 src/arch/x86_64/arch.x86_64.context.pas create mode 100644 src/arch/x86_64/arch.x86_64.debug.pas create mode 100644 src/arch/x86_64/arch.x86_64.irq.pas create mode 100644 src/arch/x86_64/arch.x86_64.isr.pas create mode 100644 src/arch/x86_64/arch.x86_64.util.pas create mode 100644 src/arch/x86_64/boot/stub.asm create mode 100644 src/arch/x86_64/isr/entry.asm create mode 100644 src/arch/x86_64/memory/arch.x86_64.memory.physical.pas create mode 100644 src/arch/x86_64/memory/arch.x86_64.memory.virtual.pas create mode 100644 src/arch/x86_64/platform/arch.platform.cpu.pas create mode 100644 src/arch/x86_64/platform/arch.platform.diagnostics.pas create mode 100644 src/arch/x86_64/platform/arch.platform.memory.pas create mode 100644 src/arch/x86_64/platform/arch.platform.process.pas create mode 100644 src/arch/x86_64/proc/arch.x86_64.proc.sched.pas create mode 100644 src/arch/x86_64/system.pas create mode 100644 src/boot/boot.multiboot.pas create mode 100644 src/core/ds/core.ds.dynamic.pas create mode 100644 src/debug/debug.early.pas create mode 100644 src/io/io.directory.pas create mode 100644 src/io/io.types.pas create mode 100644 src/memory/memory.bitmap.pas create mode 100644 src/memory/memory.raw.pas create mode 100644 src/proc/proc.scheduler.pas create mode 100644 toolchain/check_arch_boundaries.py create mode 100644 toolchain/target.sh create mode 100644 toolchain/x86_64/linker.script create mode 100644 toolchain/x86_64/test_boot.ps1 diff --git a/.gitignore b/.gitignore index e02fde13..faafcedb 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,8 @@ /lib/*.ppu /lib/*.a /lib/*.o +/lib/x86_64/ +/bin/x86_64/ /release/*.svg /release/*.iso /bin/*.bin diff --git a/Dockerfile b/Dockerfile index 66febdd2..24fdaea4 100644 --- a/Dockerfile +++ b/Dockerfile @@ -6,7 +6,7 @@ ENV DEBIAN_FRONTEND=noninteractive RUN dpkg --add-architecture i386 RUN apt-get update && apt-get install -y \ curl dos2unix wget git make nasm binutils xorriso grub-pc-bin gcc gcc-multilib \ - python3 python3-pip python3-pil && \ + python3 python3-pip python3-pil fp-compiler && \ apt-get clean my room RUN pip3 install --no-cache-dir --break-system-packages "mkdocs>=1.6,<2" mkdocs-material @@ -20,6 +20,6 @@ RUN curl -sL https://sourceforge.net/projects/freepascal/files/Linux/$FPC_VERSIO ADD https://raw.githubusercontent.com/fsaintjacques/semver-tool/master/src/semver /usr/bin/semver RUN chmod +x /usr/bin/semver WORKDIR /code -RUN find . -type f -print0 | xargs -0 dos2unix +# Sources are mounted at runtime; the CMD below normalizes build scripts. ENTRYPOINT ["/bin/bash", "-c"] -CMD ["find toolchain -name '*.sh' -exec dos2unix {} + 2>/dev/null; find toolchain -name '*.sh' -exec chmod +x {} +; bash toolchain/compile.sh"] \ No newline at end of file +CMD ["find toolchain -name '*.sh' -exec dos2unix {} + 2>/dev/null; find toolchain -name '*.sh' -exec chmod +x {} +; bash toolchain/compile.sh"] diff --git a/doc/planning/x86_64.md b/doc/planning/x86_64.md new file mode 100644 index 00000000..ed5ae08c --- /dev/null +++ b/doc/planning/x86_64.md @@ -0,0 +1,353 @@ +# x86-64 migration + +## Status + +The x86-64 target boots into Pascal with exception handling, physical/virtual +allocation, the shared heap, PIC/PIT interrupts and tested kernel-context +preemption. Fatal faults report over COM1. +This is a bootstrap kernel, not the complete Asuro desktop or storage stack. +The existing x86 target remains the default. + +## Architecture boundaries and reuse + +Follow the namespace convention in [namespace.md](namespace.md): architecture +units live under `src/arch//` and use `arch..*` names. +The compiler-required `system.pas` retains its special name. + +- `src/arch/x86_64/boot/stub.asm`: 32-bit Multiboot entry and long-mode transition. +- `src/arch/x86_64/arch.x86_64.boot.pas`: bootstrap entry and serial self-checks. +- `src/arch/x86_64/system.pas`: minimal compiler runtime definitions. +- `src/arch/x86_64/arch.x86_64.isr.pas`: IDT/TSS setup, frame validation, and + allocation-free exception reporting. +- `src/arch/x86_64/isr/entry.asm`: generated entry stubs, register/FX save and + restore, `iretq`, emergency stacks, and controlled fault probes. +- `src/debug/debug.early.pas`: reusable architecture-independent diagnostic + formatting, supplied with a character sink by `arch.x86_64.debug`. +- `src/arch/common/system.types.inc`: shared scalar/compiler definitions extracted + from the existing system unit; native widths follow the selected CPU target. +- `toolchain/target.sh`: target selection shared by the existing build steps. +- `toolchain/x86_64/linker.script`: bootstrap address layout. + +Do not copy the i386 kernel, drivers, heap, scheduler policy, or runtime wholesale. +As each subsystem is enabled, retain portable logic in its existing namespace +and put architecture-specific operations behind the appropriate architecture +interface. External `lvgl/` and `wasuro/` sources must remain unchanged. + +## Build and boot + +Rebuild the builder image when first adding the native x86-64 FPC compiler: + +```powershell +docker-compose down --remove-orphans +docker-compose build builder +``` + +Build x86-64 using the normal build command with an explicit target: + +```powershell +$env:ASURO_ARCH = 'x86_64' +docker-compose run builder | Tee-Object -FilePath .\build.log +Remove-Item Env:ASURO_ARCH +Select-String -Path .\build.log -Pattern "Error|Fatal|Success|No errors|Failed" -CaseSensitive +``` + +Outputs are `Asuro-x86_64.iso`, `bin/x86_64/kernel.bin`, and `lib/x86_64/`. +Without `ASURO_ARCH`, the existing x86 build still produces `Asuro.iso`, +`bin/kernel.bin`, and `lib/`. Cleaning and linking only consider the selected +target's objects. The x86 compiler search path excludes `src/arch/x86_64`. + +Use VirtualBox for runtime validation. With the configured Asuro VM stopped, +select `Asuro-x86_64.iso` as its optical medium, then start it through the normal +VM workflow and read `C:\Temp\asuro.log`. Restore the original optical medium +after testing. The bootstrap does not access storage controllers or disks. + +Expected serial output: + +```text +ASURO x86_64: IDT/TSS ready; DF/NMI/MC emergency stacks installed +ASURO x86_64: entered Pascal +PASS: long mode, Multiboot handoff, 64-bit Pascal types +PASS: INT3 frame, GPRs, RFLAGS, x87/SSE and IRETQ +PASS: page-fault error code, CR2 and IRETQ recovery +EXCEPTION_TESTS_PASSED=0000000000000002 +EXCEPTION_TESTS_FAILED=0000000000000000 +ASURO x86_64: bootstrap complete; interrupts disabled +``` + +An early single `C` means CPUID/long-mode support is unavailable; `B` means an +invalid Multiboot magic value. The Pascal checks validate the handoff magic, +a non-null info pointer, native/fixed type widths, wide arithmetic, and EFER.LMA. + +### Repeatable VirtualBox fault checks + +The default build runs two returning self-tests. `INT3` verifies the saved frame, +all 15 general registers including high halves, CF/DF, x87 and XMM0/XMM15 state, +and `iretq`. The handler intentionally destroys FX state before the return path +restores it. A write to unmapped `0x100000000` verifies hardware error code `2`, +CR2, and recovery by changing RIP only for the exact expected probe. + +Use the bounded test harness with the configured VM powered off: + +```powershell +.\toolchain\x86_64\test_boot.ps1 -Test none +``` + +For deliberately fatal tests, select `ud`, `pf`, or `df` when building, then use +the matching harness option. For example: + +```powershell +$env:ASURO_ARCH = 'x86_64' +$env:ASURO_BOOT_TEST = 'df' +docker-compose run builder | Tee-Object -FilePath .\build.log +Remove-Item Env:ASURO_BOOT_TEST +Remove-Item Env:ASURO_ARCH +.\toolchain\x86_64\test_boot.ps1 -Test df +``` + +The harness uses `localenv.json`, temporarily attaches the ISO, starts the VM +through `virtualbox-wrapper.ps1 -command up -Headless -NoMonitor`, validates +serial output, captures registers, stops the VM, and restores the original +optical medium in a `finally` block. The new wrapper switches are optional; +the existing interactive workflow is unchanged. Storage disks are not changed. + +Rebuild without `ASURO_BOOT_TEST` after testing to restore the normal image. + +## Bootstrap layout and limitations + +- GRUB loads the kernel at physical/virtual 1 MiB using explicit Multiboot v1 + load-address fields; the payload is ELF64 with a 32-bit assembly entry. +- Six reserved 4 KiB pages hold the PML4, PDPT, and four page directories. + They identity-map the first 4 GiB with 2 MiB leaves. +- A reserved 16 KiB stack is aligned for the SysV AMD64 calling convention. + These bounded early allocations exist before any heap and must remain + reserved when the physical allocator is introduced. +- Paging, PAE, long mode, x87, and SSE2 support are enabled before calling Pascal. +- A 256-entry IDT uses normalized 64-bit frames. TSS IST1 supplies a 16 KiB + double-fault stack; IST2 and IST3 supply separate 8 KiB NMI and machine-check + stacks. The 4 KiB IDT, 104-byte TSS, and emergency stacks are bounded early + reservations before the allocator exists. Stack guard pages remain future work. +- Entry preserves GPRs and captures CR2 before Pascal code. It clears DF, aligns + the call stack, and saves/restores x87/SSE state using FXSAVE64/FXRSTOR64. + AVX/OSXSAVE and lazy-FPU switching are not enabled. Future changes must extend + this state policy before enabling those features. +- IRQ0 is enabled during timer and context tests, then masked before the final + halt. PIC acknowledgement and spurious IRQ7/15 handling are implemented. + General driver hooks, APIC/SMP, Pascal unit-init + dispatch remain future work. +- The low identity map is temporary. It is not a final process address-space + design and does not enforce page-level W^X. New heap mappings use 4 KiB pages + at `0xFFFF800000000000`, spanning up to 16 GiB with NX when supported. + Their backing frames can reside above 4 GiB. +- Unsupported runtime operations intentionally fail to link rather than silently + delegating to a hosted runtime or pretending to succeed. + +## Remaining migration work + +| Area | Existing assumptions / next work | +|---|---| +| Runtime | Keep compiler metadata shared; implement required x86-64 helpers and initialization dispatch as consumers are ported. | +| Common kernel entry | `src/asuro.pas` currently imports x86 units; separate common boot sequencing from architecture setup before sharing full initialization. | +| Exceptions | IDT/TSS, IST stacks, normalized frames, diagnostics, and tested `iretq` paths are implemented. NMI/MC injection, user transitions, guard pages, and general IRQ dispatch remain. | +| ABI | Bootstrap interrupt alignment and x87/SSE preservation are tested. Audit red-zone assumptions for each added compiler/library and extend preservation before enabling AVX or lazy-FPU switching. | +| Memory | PMM, VMM and shared heap implemented. Final process layout, guards, MMIO and DMA APIs remain. | +| Scheduler/loader | Shared scheduling, kernel preemption and process lifecycle tested. Process address spaces and executable loading remain. | +| Drivers/storage | Audit `uint32(pointer)` conversions, MMIO and DMA translations, and hardware address limits. Keep disk/USB/controller structures fixed-width. | +| Graphics | VESA calls into V86, which cannot operate in long mode. Initially use a bootloader framebuffer; rebuild LVGL and verify Pascal/C bindings later. | +| Validation | Add deliberate exception tests, allocator checks, preemption checks, and storage tests as their implementations become available. | + +Next milestone: port filesystem/console services and complete driver address/DMA +audits. Kernel process lifecycle now uses the shared manager. Do not enable the +desktop/storage stack before its dependencies are ported and tested. + +## Validation performed — 2026-11-07 + +- x86-64 compile, link, GRUB header validation, and ISO generation passed. +- The existing Asuro VirtualBox VM printed all three expected serial lines. +- First-milestone VirtualBox registers showed `CS=0x18`, `CR0=0x80000013`, + `CR3=0x103000`, `CR4=0x620`, and `EFER=0x500` (LME and LMA set). +- Disassembly confirmed 64-bit Pascal code, wide arithmetic, and aligned calls + in the bootstrap. This does not establish interrupt safety for future code. +- The full default x86 build passed, including compilation, linking, ISO, and + documentation generation, with the x86-64 output directory still present. +- The VM was stopped and its original optical medium restored after testing. +- VirtualBox passed the normal boot tests and deliberate invalid-opcode, + page-fault, and double-fault builds. The final write-fault test preserved + error code `2` and CR2 `0x100000000`. +- The exception milestone's normal boot captured `TR=0x20`, confirming the + installed TSS selector, with long mode still active (`EFER=0x500`). +- Double-fault injection set RSP to unmapped `0x100001000`. Diagnostics reported + vector `8`, error `0`, the original RSP, and a frame inside the IST1 stack. +- Fatal exception reports include all 15 GPRs, RIP/CS/RFLAGS/RSP/SS, error code, + CR2, and frame address. CR2 is the last page-fault address and may be stale + for other exception types. + +Runtime evidence is saved locally in the ignored `bin/x86_64/boot.log` and +`bin/x86_64/registers.log` files for the first milestone, and `boot-.log` / +`registers-.log` for the exception milestone. Unsupported-CPU boot checks and NMI/MC injection have not yet been performed. + +The interrupt-frame and IST layout follows the +[AMD64 System Programming manual, chapter 8](https://www.amd.com/content/dam/amd/en/documents/processor-tech-docs/programmer-references/24593.pdf). + +## Memory, timer and shared scheduling progress + +The PMM uses 4 KiB frames and dynamically placed bitmaps, with a 64 GiB management +cap. Boot metadata, modules, kernel ranges, low bootstrap memory and allocator +metadata stay reserved. VMM allocates high virtual arenas and rolls back both +page tables and backing frames on failure. Page tables remain below 4 GiB; +backing frames may reside above it. + +`memory.heap` is shared between targets, with native-width pointer/size APIs, +16-byte alignment, overflow rejection and large-allocation rollback. +`memory.bitmap` supplies the common bitmap operations. `boot.multiboot` owns +fixed-width protocol records, with aliases retained in the x86 boot unit. +`arch.common.legacyirq` shares PIC programming and PIT constants. + +`io.types` separates data contracts from console initialization. `proc.types` +uses native saved-stack addresses. The existing round-robin/quantum policy now +lives in `proc.scheduler`, called by both the i386 process manager and x86-64 +preemption tests. Process creation, messaging, return/exit, resource cleanup and reaping now use +`proc.mgr` on x86-64. Descriptor tables are now active; console commands, executable loading and +full filesystem/controller integration remain unavailable in the bootstrap. + +VirtualBox validation has passed at 64 MiB, 2 GiB and 6 GiB. The 6 GiB run mapped, +wrote, read and released RAM above 4 GiB. Tests cover PMM accounting and invalid +frees, VMM rollback under injected allocation failures, heap alignment/zeroing/ +large allocations, timer delivery, spurious IRQ7/15, IF preservation and CLI. +Two heap-backed kernel contexts preserve distinct 64-bit registers, x87 and XMM +state across timer preemption, then release their stacks after returning to the +bootstrap context. Only the BSP is active; IRQ exclusion is not an SMP lock. + +```powershell +.\toolchain\x86_64\test_boot.ps1 -Test none -MemoryMB 64 +.\toolchain\x86_64\test_boot.ps1 -Test none -MemoryMB 6144 +``` + +The harness restores original VM RAM and optical media. Logs use +`bin/x86_64/boot--.log` and matching register filenames; zero means +use the configured RAM. All subsystem `*_TESTS_FAILED` fields must be zero. +PIC handling follows the [Intel 8259A datasheet](https://www.cs.umb.edu/cs341/Intel8259/I8259APIC.pdf). + +The final i386 regression build passed compilation, linking, ISO and documentation +steps. VirtualBox reached `Boot complete - halting into IDLE`; reported boot test +suites had zero failures. Evidence: `bin/x86_64/i386-regression.log`. +Fatal UD, write-PF and broken-stack DF tests also passed after the context-return +path was extended. Final normal images are built without fault injection. + +## Shared process lifecycle + +`proc.mgr` now runs on both targets with native process-pointer slots, stable +monotonic IDs, suspended creation, messaging, exit and resource ownership. +Initial stack layout lives in the architecture context units. x86-64's scheduler +adapter saves the current stack and calls the same scheduler/manager as i386. +A shared reaper process handles destruction on both targets outside IRQ context. +Timer selection and kill requests coalesce worker wakeups; cleanup runs after +registry/resource detachment. The reaper sleeps while there is no pending work. + +`core.ds.dynamic` contains the common dynamic-list implementation; existing +`core.ds.lists` exports compatible aliases/wrappers. Native addresses, checked +capacity arithmetic, overlap-safe insertion and empty-list regrowth replace +32-bit arithmetic. Deletion retains capacity; explicit `DL_ShrinkToFit` frees +excess storage without allocating inside every scheduler deletion. +`memory.raw` supplies allocation-free scalar fill/move operations. + +Process tests cover suspended publication, high-address lookup, killed tasks +never running, normal return, explicit exit, child notification waking an +awaiting parent, callback reentry without double cleanup, command argument/ +stream publication, sleep using the PIT divisor, and restored heap accounting. +The full i386 build and VirtualBox boot passed after these shared changes; +evidence is in `bin/x86_64/i386-lifecycle-regression.log`. + +The dedicated reaper revision passed all 15 process/container checks at 64 MiB +and 6 GiB, including the corrected check-before-await notification protocol. +The final i386 build and VirtualBox boot also passed with the shared reaper. +VM RAM and original optical media were restored and the VM was powered off. + +## Descriptor tables + +The shared `driver.storage.fdtable` now uses native memory operations and balanced +IRQ locking. Every x86-64 process, including system tasks, owns its normal FD +table. Storage protocol types depend on shared record definitions rather than +bringing in the linked-list implementation and its console dependencies. + +Close reserves an entry until callbacks and owned-string cleanup finish, rejects +active async references and reference-count overflow, and prevents callback +reentry from freeing it twice. Table teardown atomically rejects new users before +process removal. An exited process with active FD references remains alive; final +completion only wakes the shared reaper, which performs cleanup in process +context. Waiting does not repeatedly scan descriptors on every timer tick. + +VirtualBox passes 15 descriptor checks plus 16 lifecycle/container checks, +including an exited child held alive by an async FD reference until another task +releases it. No disk I/O is performed by these bootstrap tests. This validates +FD ownership, not full VFS operation or controller DMA support in long mode. + +The descriptor-table revision also passes the complete i386 build and VirtualBox +boot, including the existing storage/VFS suites and normal volume discovery. +Evidence: `bin/x86_64/i386-fd-regression.log`. Original VM settings are restored. + +## Shared strings and console command infrastructure + +`core.util` memory helpers accept native addresses and sizes, reusing scalar +`memory.raw` operations while retaining i386's existing large-copy REP path. +`core.strings` no longer imports the x86 utility or linked-list implementation; +all string copy addresses are native. Its common tests cover signed 64-bit +minimum formatting, overflowing substring/match bounds, and terminator size +rejection. The x86-64 system unit supplies Double-to-int64 truncation using +CVTTSD2SI without changing rounding control state. + +`io.stdio` now builds on both targets: growable buffers, bounded command parsing, +case-insensitive allocation-free lookup, registry growth, ANSI output, and +ECHO/CLEAR/HELP share one implementation. Buffer appends reserve space in bulk +and support self-append across relocation. Allocation failures retain the old +buffer; parser failures release all partially built parameters. Completion +detaches the halt owner before invoking callbacks, allowing safe reentry. + +VFS working-directory delegation and VERSION/TIME/REBOOT registration remain +gated until their platform dependencies are integrated. This is command +infrastructure, not yet an interactive keyboard/display/serial shell. No VFS or +storage-controller support is implied by compiling the console unit. + +The 64-bit bootstrap runs 162 shared string tests and 17 stdio tests. The process +suite also launches the registered ECHO through `runCommand` with real parsed +arguments and a growable output buffer, bringing it to 17 lifecycle/container +checks. The 6 GiB VirtualBox run verifies scheduled ECHO and physical RAM above +4 GiB. The complete i386 build and boot also pass, including 162 string tests, +stdio tests, normal mounts, and the existing storage/VFS suites. Evidence: +`bin/x86_64/i386-console-regression.log` and the normal x86-64 boot logs. + +## Build-selected platform adapters + +Shared Pascal units no longer select CPUs with `CPU64`/`CPU386` conditionals. +`ASURO_ARCH` selects a single `src/arch//platform` directory in the +compiler search path. Both targets implement the same `arch.platform.cpu`, +`arch.platform.memory`, `arch.platform.process`, and +`arch.platform.diagnostics` unit names. Their declarations are shared through +`src/arch/common/platform.*.inc`; implementations delegate to the existing +architecture units. Calls are resolved at compile/link time, with no runtime +architecture selection or function-pointer dispatch. + +The bitmap heap, allocation headers, process registry, scheduling policy, +strings, console buffers/parser, and descriptor lifecycle remain single shared +implementations. Native scalar widths live in each selected system unit. +`memory.raw` builds its fill pattern from native word width without a CPU branch. + +`arch.x86.startup` now registers heap/process/console startup with the boot +manager. `arch.x86.console` owns VERSION/TIME/REBOOT commands and their driver +dependencies. The x86-64 bootstrap retains its explicit startup sequence. +Unused working-directory wrappers moved from `io.stdio` into `io.directory`, +which explicitly depends on VFS. Existing in-repository callers need no changes. + +Every normal build runs `toolchain/check_arch_boundaries.py` to reject CPU +conditionals in shared units and direct architecture imports in migrated units. +Existing `DEBUG_LMM` allocator guards and architecture-local fault-injection +build flags remain; these are feature/test options, not architecture selection. + +Validation: complete i386 build and VirtualBox boot, including 17 stdio checks, +162 string checks, storage/VFS suites and normal mounts. Evidence: +`bin/x86_64/i386-platform-regression.log`. x86-64 boot validation covers heap, +IRQs, contexts, descriptors, strings, console and scheduled command execution. + +The final adapter build also passes the 6 GiB VirtualBox bootstrap, including +physical memory above 4 GiB and all 162 string, 17 console, 15 descriptor, +and 17 lifecycle/container checks. VM RAM and optical media were restored; +the VM is powered off. diff --git a/docker-compose.yml b/docker-compose.yml index 845189bd..3524a579 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,5 +1,8 @@ services: builder: build: . + environment: + ASURO_ARCH: ${ASURO_ARCH:-x86} + ASURO_BOOT_TEST: ${ASURO_BOOT_TEST:-none} volumes: - - .:/code \ No newline at end of file + - .:/code diff --git a/mkdocs.yml b/mkdocs.yml index cba7f458..4c920e6f 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -295,6 +295,7 @@ nav: - Planning: - Architecture: planning/architecture.md + - x86-64 Migration: planning/x86_64.md - USB: planning/USB.md - XHCI: planning/XHCI.md - FlatFS: planning/flatfs.md diff --git a/src/arch/common/arch.common.interrupts.pas b/src/arch/common/arch.common.interrupts.pas new file mode 100644 index 00000000..71b634e1 --- /dev/null +++ b/src/arch/common/arch.common.interrupts.pas @@ -0,0 +1,23 @@ +// Copyright 2026 Aaron Hance +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +unit arch.common.interrupts; +interface +function lock: PtrUInt; +procedure unlock(flags: PtrUInt); +implementation +uses arch.platform.cpu; +function lock: PtrUInt; +begin lock := arch.platform.cpu.lock; end; +procedure unlock(flags: PtrUInt); +begin arch.platform.cpu.unlock(flags); end; +end. diff --git a/src/arch/common/arch.common.legacyirq.pas b/src/arch/common/arch.common.legacyirq.pas new file mode 100644 index 00000000..86fcfd5d --- /dev/null +++ b/src/arch/common/arch.common.legacyirq.pas @@ -0,0 +1,69 @@ +// Copyright 2026 Aaron Hance +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +{ Shared PC interrupt-controller programming. Call with interrupts disabled. } +unit arch.common.legacyirq; +interface +const + PIT_INPUT_HZ = 1193182; + PIT_DIVISOR = 149; +procedure remap(masterMask, slaveMask: uint8); +procedure masks(masterMask, slaveMask: uint8); +procedure timerStart; +function realIRQ(line: uint8): boolean; +procedure acknowledge(line: uint8); +implementation +uses arch.platform.cpu; +procedure writeSlow(port: uint16; value: uint8); +begin + outb(port, value); + outb($80, 0); +end; +procedure masks(masterMask, slaveMask: uint8); +begin + writeSlow($21, masterMask); writeSlow($A1, slaveMask); +end; +procedure remap(masterMask, slaveMask: uint8); +begin + writeSlow($20, $11); writeSlow($A0, $11); + writeSlow($21, $20); writeSlow($A1, $28); + writeSlow($21, $04); writeSlow($A1, $02); + writeSlow($21, $01); writeSlow($A1, $01); + masks(masterMask, slaveMask); +end; +procedure timerStart; +begin + { Channel 0, low/high byte, mode 2, binary. } + outb($43, $34); + outb($40, PIT_DIVISOR and $FF); + outb($40, PIT_DIVISOR shr 8); +end; +function realIRQ(line: uint8): boolean; +begin + realIRQ := line < 16; + if line = 7 then begin + outb($20, $0B); + realIRQ := (inb($20) and $80) <> 0; + end else if line = 15 then begin + outb($A0, $0B); + realIRQ := (inb($A0) and $80) <> 0; + { A spurious slave request still put master IRQ2 in service. } + if not realIRQ then outb($20, $20); + end; +end; +procedure acknowledge(line: uint8); +begin + if line >= 16 then exit; + if line >= 8 then outb($A0, $20); + outb($20, $20); +end; +end. diff --git a/src/arch/common/platform.cpu.inc b/src/arch/common/platform.cpu.inc new file mode 100644 index 00000000..e712aa7d --- /dev/null +++ b/src/arch/common/platform.cpu.inc @@ -0,0 +1,20 @@ +// Copyright 2026 Aaron Hance +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +{ Shared contract; implemented by each build-selected adapter. } +function lock: PtrUInt; +procedure unlock(flags: PtrUInt); +procedure outb(port: uint16; value: uint8); +function inb(port: uint16): uint8; +procedure copyBytes(source, dest: PtrUInt; bytes: SizeUInt); +procedure zero(dest: pointer; bytes: SizeUInt); +procedure idle; diff --git a/src/arch/common/platform.diagnostics.inc b/src/arch/common/platform.diagnostics.inc new file mode 100644 index 00000000..8a857356 --- /dev/null +++ b/src/arch/common/platform.diagnostics.inc @@ -0,0 +1,18 @@ +// Copyright 2026 Aaron Hance +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +{ Shared contract; implemented by each build-selected adapter. } +procedure log(tag, message: pchar); +procedure push_trace(name: pchar); +procedure pop_trace; +procedure fatal(tag, message: pchar); +procedure testSummary(tag, passedKey, failedKey: pchar; passed, failed: uint32); diff --git a/src/arch/common/platform.memory.inc b/src/arch/common/platform.memory.inc new file mode 100644 index 00000000..9523a52a --- /dev/null +++ b/src/arch/common/platform.memory.inc @@ -0,0 +1,18 @@ +// Copyright 2026 Aaron Hance +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +{ Shared contract; implemented by each build-selected adapter. } +function allocateHeapPage: PtrUInt; +function allocateLarge(pages: uint32): PtrUInt; +procedure release(address: PtrUInt; pages: uint32); +function identityReserve(address: PtrUInt): pointer; +function freeBacking: uint64; diff --git a/src/arch/common/platform.process.inc b/src/arch/common/platform.process.inc new file mode 100644 index 00000000..80023dfd --- /dev/null +++ b/src/arch/common/platform.process.inc @@ -0,0 +1,15 @@ +// Copyright 2026 Aaron Hance +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +{ Shared contract; implemented by each build-selected adapter. } +function prepare(stack: pointer; bytes: SizeUInt; entry: pointer): PtrUInt; +function timerTicks: uint32; diff --git a/src/arch/common/system.types.inc b/src/arch/common/system.types.inc new file mode 100644 index 00000000..d2072335 --- /dev/null +++ b/src/arch/common/system.types.inc @@ -0,0 +1,197 @@ +// Copyright 2021 Kieron Morris & Aaron Hance +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +{ Shared compiler and scalar types. Included by each architecture system unit. } +type + //internal core.types + cardinal = 0..$FFFFFFFF; + hresult = type longint; + dword = cardinal; + integer = longint; + pchar = ^char; + + //Standard Types + uInt8 = BYTE; + uInt16 = WORD; + uInt32 = DWORD; + uInt64 = QWORD; + uint128 = packed record + case Integer of + 0: ( + Hi : uint64; + Lo : uint64; + ); + 1: ( + DWords : array [0..3] of uint32; + ); + 2: ( + Words : array [0..7] of uint16; + ); + 3: ( + Bytes : array [0..15] of uint8; + ); + end; + + sInt8 = shortint; + sInt16 = smallint; + sInt32 = integer; + sInt64 = int64; + + Float = Single; + + //Pointer Types + PuByte = ^Byte; + PuInt8 = PuByte; + PuInt16 = ^uInt16; + PuInt32 = ^uInt32; + PuInt64 = ^uInt64; + PuInt128 = ^uint128; + + PsInt8 = ^sInt8; + PsInt16 = ^sInt16; + PsInt32 = ^sInt32; + PsInt64 = ^sInt64; + + PFloat = ^Float; + PDouble = ^Double; + + Void = ^uInt32; + + yord = uInt8; + xord = uInt8; + zord = uInt16; + + //Alternate Types + UBit1 = 0..(1 shl 01) - 1; + UBit2 = 0..(1 shl 02) - 1; + UBit3 = 0..(1 shl 03) - 1; + UBit4 = 0..(1 shl 04) - 1; + UBit5 = 0..(1 shl 05) - 1; + UBit6 = 0..(1 shl 06) - 1; + UBit7 = 0..(1 shl 07) - 1; + UBit9 = 0..(1 shl 09) - 1; + UBit10 = 0..(1 shl 10) - 1; + UBit11 = 0..(1 shl 11) - 1; + UBit12 = 0..(1 shl 12) - 1; + UBit13 = 0..(1 shl 13) - 1; + UBit14 = 0..(1 shl 14) - 1; + UBit15 = 0..(1 shl 15) - 1; + UBit16 = 0..(1 shl 16) - 1; + UBit17 = 0..(1 shl 17) - 1; + UBit18 = 0..(1 shl 18) - 1; + UBit19 = 0..(1 shl 19) - 1; + UBit20 = 0..(1 shl 20) - 1; + UBit21 = 0..(1 shl 21) - 1; + UBit22 = 0..(1 shl 22) - 1; + UBit23 = 0..(1 shl 23) - 1; + UBit24 = 0..(1 shl 24) - 1; + UBit25 = 0..(1 shl 25) - 1; + UBit26 = 0..(1 shl 26) - 1; + UBit27 = 0..(1 shl 27) - 1; + UBit28 = 0..(1 shl 28) - 1; + UBit30 = 0..(1 shl 30) - 1; + UBit31 = 0..(1 shl 31) - 1; + + TBitMask = bitpacked record + b0,b1,b2,b3,b4,b5,b6,b7 : Boolean; + end; + PBitMask = ^TBitMask; + + TMask = bitpacked array[0..7] of Boolean; + PMask = ^TMask; + + PText = ^Text; + + { ---- Types required by FPC 3.2.2 compiler internals ---- } + + { Native widths follow the selected compiler target. Fixed-width types above + remain unchanged for hardware descriptors and on-disk structures. } + PtrInt = SizeInt; + PtrUInt = SizeUInt; + ValSInt = SizeInt; + ValUInt = SizeUInt; + NativeInt = SizeInt; + NativeUInt = SizeUInt; + CodePointer = Pointer; + PCodePointer = ^CodePointer; + PSizeInt = ^SizeInt; + + { RTTI type kind enumeration — must match compiler's internal list } + TTypeKind = ( + tkUnknown, tkInteger, tkChar, tkEnumeration, tkFloat, + tkSet, tkMethod, tkSString, tkLString, tkAString, + tkWString, tkVariant, tkArray, tkRecord, tkInterface, + tkClass, tkObject, tkWChar, tkBool, tkInt64, + tkQWord, tkDynArray, tkInterfaceRaw, tkProcVar, tkUString, + tkUChar, tkHelper, tkFile, tkClassRef, tkPointer + ); + + { GUID record — required by compiler for interface support } + PGuid = ^TGuid; + TGuid = packed record + case Integer of + 1: (Data1: DWord; Data2: Word; Data3: Word; Data4: array[0..7] of Byte); + 2: (D1: DWord; D2: Word; D3: Word; D4: array[0..7] of Byte); + 3: (time_low: DWord; time_mid: Word; time_hi_and_version: Word; + clock_seq_hi_and_reserved: Byte; clock_seq_low: Byte; + node: array[0..5] of Byte); + end; + + { File record core.types — required by compiler for file I/O internals } + FileRec = packed record + Handle : longint; + Mode : longint; + RecSize : SizeInt; + _private : array[0..31] of byte; + UserData : array[0..31] of byte; + name : array[0..255] of char; + end; + PFileRec = ^FileRec; + + TextBuf = array[0..255] of char; + TextRec = packed record + Handle : longint; + Mode : longint; + BufSize : SizeInt; + _private : SizeInt; + BufPos : SizeInt; + BufEnd : SizeInt; + BufPtr : ^TextBuf; + OpenFunc : CodePointer; + InOutFunc : CodePointer; + FlushFunc : CodePointer; + CloseFunc : CodePointer; + UserData : array[0..31] of byte; + name : array[0..255] of char; + LineEnd : array[0..3] of char; + Buffer : TextBuf; + end; + PTextRec = ^TextRec; + + { FPC 3.2.2 INITFINAL table — compiler-generated unit init/finalize table. + Each unit with an initialization or finalization section gets an entry. + The table is emitted by the compiler under the symbol 'INITFINAL'. } + TInitFinalRec = packed record + InitProc : CodePointer; + FinalProc: CodePointer; + end; + + TInitFinalTable = packed record + TableCount : longint; + Procs : array[1..1] of TInitFinalRec; { open array, actual size = TableCount } + end; + PInitFinalTable = ^TInitFinalTable; + + { Procedure type for calling initialization procs from the INITFINAL table } + TProcedure = procedure; diff --git a/src/arch/x86/arch.x86.console.pas b/src/arch/x86/arch.x86.console.pas new file mode 100644 index 00000000..bb8b1658 --- /dev/null +++ b/src/arch/x86/arch.x86.console.pas @@ -0,0 +1,82 @@ +// Copyright 2026 Aaron Hance +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +{ Commands extracted from io.stdio (Kieron Morris). } +unit arch.x86.console; +interface +procedure init; +implementation +uses io.stdio, core.version, driver.timer.rtc, arch.x86.util; +procedure cmd_version(params: PParamList; stdin_buf, stdout_buf, stderr_buf: POutBuf); +begin + bufWriteStr(stdout_buf, ' Asuro Version: '); + bufWriteStrLn(stdout_buf, core.version.VERSION); + bufWriteStr(stdout_buf, ' Compiled on: '); + bufWriteStr(stdout_buf, core.version.COMPILE_DATE); + bufWriteStr(stdout_buf, ' '); + bufWriteStrLn(stdout_buf, core.version.COMPILE_TIME); + bufWriteStrLn(stdout_buf, ' Compiled With: '); + bufWriteStr(stdout_buf, ' NASM - Version: '); + bufWriteStrLn(stdout_buf, core.version.NASM_VERSION); + bufWriteStr(stdout_buf, ' FPC - Version: '); + bufWriteStrLn(stdout_buf, core.version.FPC_VERSION); + bufWriteStr(stdout_buf, ' MAKE - Version: '); + bufWriteStrLn(stdout_buf, core.version.MAKE_VERSION); + bufWriteStr(stdout_buf, ' '); + bufWriteInt(stdout_buf, core.version.LINE_COUNT); + bufWriteStr(stdout_buf, ' lines, across '); + bufWriteInt(stdout_buf, core.version.FILE_COUNT); + bufWriteStrLn(stdout_buf, ' files.'); + bufWriteStr(stdout_buf, ' Baked Drivers: '); + bufWriteIntLn(stdout_buf, core.version.DRIVER_COUNT); + bufWriteStr(stdout_buf, ' Checksum: '); + bufWriteStrLn(stdout_buf, core.version.CHECKSUM); +end; + +procedure cmd_time(params: PParamList; stdin_buf, stdout_buf, stderr_buf: POutBuf); +var + dt: TDateTime; +begin + dt := getDateTime; + if dt.Day < 10 then bufWriteChar(stdout_buf, '0'); + bufWriteInt(stdout_buf, dt.Day); + bufWriteChar(stdout_buf, '/'); + if dt.Month < 10 then bufWriteChar(stdout_buf, '0'); + bufWriteInt(stdout_buf, dt.Month); + bufWriteChar(stdout_buf, '/'); + bufWriteInt(stdout_buf, dt.Century); + bufWriteInt(stdout_buf, dt.Year); + bufWriteChar(stdout_buf, ' '); + if dt.Hours < 10 then bufWriteChar(stdout_buf, '0'); + bufWriteInt(stdout_buf, dt.Hours); + bufWriteChar(stdout_buf, ':'); + if dt.Minutes < 10 then bufWriteChar(stdout_buf, '0'); + bufWriteInt(stdout_buf, dt.Minutes); + bufWriteChar(stdout_buf, ':'); + if dt.Seconds < 10 then bufWriteChar(stdout_buf, '0'); + bufWriteIntLn(stdout_buf, dt.Seconds); + bufWriteStr(stdout_buf, 'Weekday: '); + bufWriteStrLn(stdout_buf, WeekdayToString(dt.Weekday)); +end; + +procedure cmd_reboot(params: PParamList; stdin_buf, stdout_buf, stderr_buf: POutBuf); +begin + resetSystem; +end; + +procedure init; +begin + registerCommand('VERSION', @cmd_version, 'Display the running version of Asuro.'); + registerCommand('TIME', @cmd_time, 'Print the current time.'); + registerCommand('REBOOT', @cmd_reboot, 'Reboot the system.'); +end; +end. diff --git a/src/arch/x86/arch.x86.irq.pas b/src/arch/x86/arch.x86.irq.pas index 37e32a7a..dc2dcab4 100644 --- a/src/arch/x86/arch.x86.irq.pas +++ b/src/arch/x86/arch.x86.irq.pas @@ -1,4 +1,5 @@ // Copyright 2021 Kieron Morris +// Copyright 2026 Aaron Hance // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -22,7 +23,7 @@ unit arch.x86.irq; interface uses - boot.mgr, + boot.mgr, arch.common.legacyirq, core.util, arch.x86.util, io.syslog; procedure init(); @@ -32,26 +33,7 @@ implementation procedure init(); begin io.syslog.logln('IRQ','INIT START.'); - outb($20, $11); - io_wait; - outb($A0, $11); - io_wait; - outb($21, $20); - io_wait; - outb($A1, $28); - io_wait; - outb($21, $04); - io_wait; - outb($A1, $02); - io_wait; - outb($21, $01); - io_wait; - outb($A1, $01); - io_wait; - outb($21, $00); - io_wait; - outb($A1, $00); - io_wait; + arch.common.legacyirq.remap(0, 0); io.syslog.logln('IRQ','INIT END.'); end; diff --git a/src/arch/x86/arch.x86.multiboot.pas b/src/arch/x86/arch.x86.multiboot.pas index e256a39c..e5fe9e48 100644 --- a/src/arch/x86/arch.x86.multiboot.pas +++ b/src/arch/x86/arch.x86.multiboot.pas @@ -1,4 +1,4 @@ -// Copyright 2021 Kieron Morris +// Copyright 2021 Kieron Morris & Aaron Hance // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -21,65 +21,22 @@ unit arch.x86.multiboot; interface +uses boot.multiboot; + const KERNEL_STACKSIZE = $4000; MULTIBOOT_BOOTLOADER_MAGIC = $2BADB002; type - Pelf_section_header_table_t = ^elf_section_header_table_t; - elf_section_header_table_t = packed record - num: uint32; - size: uint32; - addr: uint32; - shndx: uint32; - end; - - Pmultiboot_info_t = ^multiboot_info_t; - multiboot_info_t = packed record - flags: uint32; - mem_lower: uint32; - mem_upper: uint32; - boot_device: uint32; - cmdline: uint32; - mods_count: uint32; - mods_addr: uint32; - elf_sec: elf_section_header_table_t; - mmap_length: uint32; - mmap_addr: uint32; - drives_legnth : uint32; - drives_addr: uint32; - config_table : uint32; - boot_loader_name : uint32; - apm_table: uint32; - vbe_control_info : uint32; - vbe_mode_info : uint32; - vbe_mode : uint16; - vbe_interface_seg : uint16; - vbe_interface_off : uint16; - vbe_interface_len : uint16; - framebuffer_addr : uint64; - framebuffer_pitch : uint32; - framebuffer_width : uint32; - framebuffer_height: uint32; - framebuffer_bpp : uint8; - end; - - Pmodule_t = ^module_t; - module_t = packed record - mod_start: uint32; - mod_end: uint32; - name: uint32; - reserved: uint32; - end; - - Pmemory_map_t = ^memory_map_t; - memory_map_t = packed record - size: uint32; - base_addr : uint64; - length : uint64; - mtype: uint32; - end; - + elf_section_header_table_t = boot.multiboot.elf_section_header_table_t; + Pelf_section_header_table_t = boot.multiboot.Pelf_section_header_table_t; + multiboot_info_t = boot.multiboot.multiboot_info_t; + Pmultiboot_info_t = boot.multiboot.Pmultiboot_info_t; + module_t = boot.multiboot.module_t; + Pmodule_t = boot.multiboot.Pmodule_t; + memory_map_t = boot.multiboot.memory_map_t; + Pmemory_map_t = boot.multiboot.Pmemory_map_t; + var multibootinfo : Pmultiboot_info_t = nil; multibootmagic : uint32; diff --git a/src/arch/x86/arch.x86.startup.pas b/src/arch/x86/arch.x86.startup.pas new file mode 100644 index 00000000..ff9fd6a0 --- /dev/null +++ b/src/arch/x86/arch.x86.startup.pas @@ -0,0 +1,24 @@ +// Copyright 2026 Aaron Hance +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +{ Compose shared services with the i386 boot manager and platform commands. } +unit arch.x86.startup; +interface +implementation +uses boot.mgr, memory.heap, proc.mgr, io.stdio, arch.x86.console; +procedure consoleInit; +begin io.stdio.init; arch.x86.console.init; end; +initialization + boot.mgr.registerBoot('memory.heap', @memory.heap.init, 'Heap Memory Manager', 'arch.*.memory.*'); + boot.mgr.registerBoot('io.stdio', @consoleInit, 'Standard I/O Abstraction', BOOT_MGR_BARRIER_EARLY); + boot.mgr.registerBoot('proc.mgr', @proc.mgr.init, 'Process Manager', 'driver.storage.fs*'); +end. diff --git a/src/arch/x86/isr/arch.x86.isr.tmr0.pas b/src/arch/x86/isr/arch.x86.isr.tmr0.pas index 9dd86a7e..7a3773e3 100644 --- a/src/arch/x86/isr/arch.x86.isr.tmr0.pas +++ b/src/arch/x86/isr/arch.x86.isr.tmr0.pas @@ -1,4 +1,5 @@ // Copyright 2021 Kieron Morris +// Copyright 2026 Aaron Hance // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -22,14 +23,14 @@ unit arch.x86.isr.tmr0; interface uses - core.util, arch.x86.util, + core.util, arch.x86.util, arch.common.legacyirq, arch.x86.isr.types, arch.x86.isr.mgr, arch.x86.idt; const - PIT_INPUT_HZ = 1193182; - PIT_DIVISOR = 149; + PIT_INPUT_HZ = arch.common.legacyirq.PIT_INPUT_HZ; + PIT_DIVISOR = arch.common.legacyirq.PIT_DIVISOR; procedure register(); procedure hook(hook_method : uint32); diff --git a/src/arch/x86/platform/arch.platform.cpu.pas b/src/arch/x86/platform/arch.platform.cpu.pas new file mode 100644 index 00000000..eef76238 --- /dev/null +++ b/src/arch/x86/platform/arch.platform.cpu.pas @@ -0,0 +1,53 @@ +// Copyright 2026 Aaron Hance +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +{ Build-selected x86 adapter. } +unit arch.platform.cpu; +interface +{$i ../../common/platform.cpu.inc} +implementation +uses arch.x86.util, memory.raw; +function lock: PtrUInt; assembler; nostackframe; +asm pushfd; cli; pop eax end; +procedure unlock(flags: PtrUInt); +begin if (flags and $200) <> 0 then asm sti end; end; +procedure idle; +begin asm hlt end; end; +procedure outb(port: uint16; value: uint8); +begin arch.x86.util.outb(port, value); end; +function inb(port: uint16): uint8; +begin inb := arch.x86.util.inb(port); end; +procedure copyBytes(source, dest: PtrUInt; bytes: SizeUInt); +begin + if bytes >= 16 then arch.x86.util.__REP_MOVSB_memcpy(source, dest, bytes) + else memory.raw.move(pointer(source), pointer(dest), bytes); +end; +procedure zero(dest: pointer; bytes: SizeUInt); +var d: pointer; b: SizeUInt; +begin + d := dest; b := bytes; + if b = 0 then exit; + asm + push edi + mov edi, d + mov ecx, b + shr ecx, 2 + xor eax, eax + cld + rep stosd + mov ecx, b + and ecx, 3 + rep stosb + pop edi + end; +end; +end. diff --git a/src/arch/x86/platform/arch.platform.diagnostics.pas b/src/arch/x86/platform/arch.platform.diagnostics.pas new file mode 100644 index 00000000..17f664ff --- /dev/null +++ b/src/arch/x86/platform/arch.platform.diagnostics.pas @@ -0,0 +1,33 @@ +// Copyright 2026 Aaron Hance +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +{ Build-selected x86 adapter. } +unit arch.platform.diagnostics; +interface +{$i ../../common/platform.diagnostics.inc} +implementation +uses io.syslog, debug.tracer, core.panic; +procedure log(tag, message: pchar); +begin io.syslog.logln(tag, message); end; +procedure push_trace(name: pchar); +begin debug.tracer.push_trace(name); end; +procedure pop_trace; +begin debug.tracer.pop_trace; end; +procedure fatal(tag, message: pchar); +begin core.panic.panic(tag, message, nil); end; +procedure testSummary(tag, passedKey, failedKey: pchar; passed, failed: uint32); +begin + io.syslog.log(tag, ''); + io.syslog.writeint(passed); io.syslog.writestring(' passed, '); + io.syslog.writeint(failed); io.syslog.writestringln(' failed.'); +end; +end. diff --git a/src/arch/x86/platform/arch.platform.memory.pas b/src/arch/x86/platform/arch.platform.memory.pas new file mode 100644 index 00000000..74b6812b --- /dev/null +++ b/src/arch/x86/platform/arch.platform.memory.pas @@ -0,0 +1,81 @@ +// Copyright 2026 Aaron Hance +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +{ Mapping policy extracted from memory.heap (Kieron Morris). } +unit arch.platform.memory; +interface +const MAX_HEAP_REGIONS = 256; +{$i ../../common/platform.memory.inc} +implementation +uses arch.x86.memory.virtual, arch.x86.memory.physical, arch.platform.diagnostics; +function allocateHeapPage: PtrUInt; +var i: uint32; +begin + allocateHeapPage := 0; + i := KERNEL_PAGE_NUMBER + 4; + while i < 1024 do begin + if arch.x86.memory.virtual.new_page(i) then begin + allocateHeapPage := i shl 22; exit; + end; + inc(i); + end; +end; +function allocateLarge(pages: uint32): PtrUInt; +var curr_page, i: uint16; miss: boolean; +begin + allocateLarge := 0; + if (pages = 0) or (pages > MAX_HEAP_REGIONS) then exit; + curr_page := 5; + while uint32(curr_page) + pages - 1 < KERNEL_PAGE_NUMBER do begin + miss := false; + for i := 0 to pages - 1 do begin + if not page_mappable(curr_page + i) then begin + miss := true; curr_page := curr_page + i; break; + end; + end; + if not miss then begin + i := 0; + while i < pages do begin + if not arch.x86.memory.virtual.new_page(curr_page + i) then begin + while i > 0 do begin + dec(i); arch.x86.memory.virtual.free_page(curr_page + i); + end; + exit; + end; + inc(i); + end; + allocateLarge := uint32(curr_page) shl 22; + exit; + end; + inc(curr_page); + end; +end; +procedure release(address: PtrUInt; pages: uint32); +var i: uint32; page_num: uint16; +begin + if (pages = 0) or (pages > MAX_HEAP_REGIONS) then begin + fatal('LMM', 'Invalid heap region release'); exit; + end; + page_num := address shr 22; + for i := 0 to pages - 1 do arch.x86.memory.virtual.free_page(page_num + uint16(i)); +end; +function identityReserve(address: PtrUInt): pointer; +var block: uint16; +begin + block := address shr 22; + force_alloc_block(block, 0); + map_page(block, block); + identityReserve := pointer(uint32(block) shl 22); +end; +function freeBacking: uint64; +begin freeBacking := pmm_free_blocks; end; +end. diff --git a/src/arch/x86/platform/arch.platform.process.pas b/src/arch/x86/platform/arch.platform.process.pas new file mode 100644 index 00000000..1bf9910a --- /dev/null +++ b/src/arch/x86/platform/arch.platform.process.pas @@ -0,0 +1,23 @@ +// Copyright 2026 Aaron Hance +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +{ Build-selected x86 adapter. } +unit arch.platform.process; +interface +{$i ../../common/platform.process.inc} +implementation +uses arch.x86.proc.context, arch.x86.bda; +function prepare(stack: pointer; bytes: SizeUInt; entry: pointer): PtrUInt; +begin prepare := arch.x86.proc.context.prepare(stack, bytes, entry); end; +function timerTicks: uint32; +begin timerTicks := PUInt32(@Counters.c32)^; end; +end. diff --git a/src/arch/x86/proc/arch.x86.proc.context.pas b/src/arch/x86/proc/arch.x86.proc.context.pas new file mode 100644 index 00000000..a2f4429a --- /dev/null +++ b/src/arch/x86/proc/arch.x86.proc.context.pas @@ -0,0 +1,35 @@ +// Copyright 2026 Aaron Hance +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +{ i386 initial frame, extracted from proc.mgr (Kieron Morris). } +unit arch.x86.proc.context; +interface +function prepare(stack: pointer; bytes: SizeUInt; entry: pointer): PtrUInt; +implementation +uses memory.raw; +function prepare(stack: pointer; bytes: SizeUInt; entry: pointer): PtrUInt; +var top, frame: PtrUInt; i: uint32; +begin + prepare := 0; + if (stack = nil) or (entry = nil) or (bytes < 64) or + (PtrUInt(stack) > High(PtrUInt) - bytes) then exit; + top := PtrUInt(stack) + bytes; + frame := top - 60; + memory.raw.fill(pointer(frame), 0, 60); + { GS, FS, ES, DS; PUSHAD; EIP, CS, EFLAGS. } + for i := 0 to 3 do PUInt32(frame + i * 4)^ := $10; + PUInt32(top - 12)^ := PtrUInt(entry); + PUInt32(top - 8)^ := $08; + PUInt32(top - 4)^ := $0202; + prepare := frame; +end; +end. diff --git a/src/arch/x86/proc/arch.x86.proc.sched.pas b/src/arch/x86/proc/arch.x86.proc.sched.pas index 3dcc2a01..c356e0dc 100644 --- a/src/arch/x86/proc/arch.x86.proc.sched.pas +++ b/src/arch/x86/proc/arch.x86.proc.sched.pas @@ -1,4 +1,5 @@ // Copyright 2021 Kieron Morris +// Copyright 2026 Aaron Hance // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -35,7 +36,7 @@ uses procedure init; { No idle ESP variable needed — idle is now a formal process (PID 0) - with its SavedESP managed like any other process. } + with its SavedStack managed like any other process. } implementation @@ -51,7 +52,7 @@ var next : PProcessContext; begin { 1. Save ESP into the current process } - proc.mgr.CurrentProcess^.SavedESP := saved_esp; + proc.mgr.CurrentProcess^.SavedStack := saved_esp; { 2. Dispatch all hooks registered on interrupt 32 (timer hooks). This fires TMR_0_ISR.Main which in turn fires BDA tick, @@ -66,7 +67,7 @@ begin proc.mgr.CurrentProcess := next; { 5. Return the selected process ESP } - do_context_switch_work := next^.SavedESP; + do_context_switch_work := next^.SavedStack; end; { ----------------------------------------------------------------------- diff --git a/src/arch/x86/system.pas b/src/arch/x86/system.pas index d8d22ea4..8695c89c 100644 --- a/src/arch/x86/system.pas +++ b/src/arch/x86/system.pas @@ -28,131 +28,12 @@ const BSOD_ENABLE = true; TRACER_ENABLE = true; +type SizeInt = longint; + SizeUInt = longword; + +{$i ../common/system.types.inc} + type - //internal core.types - cardinal = 0..$FFFFFFFF; - hresult = type longint; - dword = cardinal; - integer = longint; - pchar = ^char; - - //Standard Types - uInt8 = BYTE; - uInt16 = WORD; - uInt32 = DWORD; - uInt64 = QWORD; - uint128 = packed record - case Integer of - 0: ( - Hi : uint64; - Lo : uint64; - ); - 1: ( - DWords : array [0..3] of uint32; - ); - 2: ( - Words : array [0..7] of uint16; - ); - 3: ( - Bytes : array [0..15] of uint8; - ); - end; - - sInt8 = shortint; - sInt16 = smallint; - sInt32 = integer; - sInt64 = int64; - - Float = Single; - - //Pointer Types - PuByte = ^Byte; - PuInt8 = PuByte; - PuInt16 = ^uInt16; - PuInt32 = ^uInt32; - PuInt64 = ^uInt64; - PuInt128 = ^uint128; - - PsInt8 = ^sInt8; - PsInt16 = ^sInt16; - PsInt32 = ^sInt32; - PsInt64 = ^sInt64; - - PFloat = ^Float; - PDouble = ^Double; - - Void = ^uInt32; - - yord = uInt8; - xord = uInt8; - zord = uInt16; - - //Alternate Types - UBit1 = 0..(1 shl 01) - 1; - UBit2 = 0..(1 shl 02) - 1; - UBit3 = 0..(1 shl 03) - 1; - UBit4 = 0..(1 shl 04) - 1; - UBit5 = 0..(1 shl 05) - 1; - UBit6 = 0..(1 shl 06) - 1; - UBit7 = 0..(1 shl 07) - 1; - UBit9 = 0..(1 shl 09) - 1; - UBit10 = 0..(1 shl 10) - 1; - UBit11 = 0..(1 shl 11) - 1; - UBit12 = 0..(1 shl 12) - 1; - UBit13 = 0..(1 shl 13) - 1; - UBit14 = 0..(1 shl 14) - 1; - UBit15 = 0..(1 shl 15) - 1; - UBit16 = 0..(1 shl 16) - 1; - UBit17 = 0..(1 shl 17) - 1; - UBit18 = 0..(1 shl 18) - 1; - UBit19 = 0..(1 shl 19) - 1; - UBit20 = 0..(1 shl 20) - 1; - UBit21 = 0..(1 shl 21) - 1; - UBit22 = 0..(1 shl 22) - 1; - UBit23 = 0..(1 shl 23) - 1; - UBit24 = 0..(1 shl 24) - 1; - UBit25 = 0..(1 shl 25) - 1; - UBit26 = 0..(1 shl 26) - 1; - UBit27 = 0..(1 shl 27) - 1; - UBit28 = 0..(1 shl 28) - 1; - UBit30 = 0..(1 shl 30) - 1; - UBit31 = 0..(1 shl 31) - 1; - - TBitMask = bitpacked record - b0,b1,b2,b3,b4,b5,b6,b7 : Boolean; - end; - PBitMask = ^TBitMask; - - TMask = bitpacked array[0..7] of Boolean; - PMask = ^TMask; - - PText = ^Text; - - { ---- Types required by FPC 3.2.2 compiler internals ---- } - - { Size/pointer integer core.types for 32-bit target } - SizeInt = longint; - SizeUInt = cardinal; - PtrInt = longint; - PtrUInt = cardinal; - ValSInt = longint; - ValUInt = cardinal; - NativeInt = longint; - NativeUInt = cardinal; - CodePointer = Pointer; - PCodePointer = ^CodePointer; - PSizeInt = ^SizeInt; - - { RTTI type kind enumeration — must match compiler's internal list } - TTypeKind = ( - tkUnknown, tkInteger, tkChar, tkEnumeration, tkFloat, - tkSet, tkMethod, tkSString, tkLString, tkAString, - tkWString, tkVariant, tkArray, tkRecord, tkInterface, - tkClass, tkObject, tkWChar, tkBool, tkInt64, - tkQWord, tkDynArray, tkInterfaceRaw, tkProcVar, tkUString, - tkUChar, tkHelper, tkFile, tkClassRef, tkPointer - ); - { jmp_buf for i386 — used by exception handling internals } jmp_buf = packed record ebx, esi, edi: LongInt; @@ -168,64 +49,6 @@ type frametype: LongInt; end; - { GUID record — required by compiler for interface support } - PGuid = ^TGuid; - TGuid = packed record - case Integer of - 1: (Data1: DWord; Data2: Word; Data3: Word; Data4: array[0..7] of Byte); - 2: (D1: DWord; D2: Word; D3: Word; D4: array[0..7] of Byte); - 3: (time_low: DWord; time_mid: Word; time_hi_and_version: Word; - clock_seq_hi_and_reserved: Byte; clock_seq_low: Byte; - node: array[0..5] of Byte); - end; - - { File record core.types — required by compiler for file I/O internals } - FileRec = packed record - Handle : longint; - Mode : longint; - RecSize : SizeInt; - _private : array[0..31] of byte; - UserData : array[0..31] of byte; - name : array[0..255] of char; - end; - PFileRec = ^FileRec; - - TextBuf = array[0..255] of char; - TextRec = packed record - Handle : longint; - Mode : longint; - BufSize : SizeInt; - _private : SizeInt; - BufPos : SizeInt; - BufEnd : SizeInt; - BufPtr : ^TextBuf; - OpenFunc : CodePointer; - InOutFunc : CodePointer; - FlushFunc : CodePointer; - CloseFunc : CodePointer; - UserData : array[0..31] of byte; - name : array[0..255] of char; - LineEnd : array[0..3] of char; - Buffer : TextBuf; - end; - PTextRec = ^TextRec; - - { FPC 3.2.2 INITFINAL table — compiler-generated unit init/finalize table. - Each unit with an initialization or finalization section gets an entry. - The table is emitted by the compiler under the symbol 'INITFINAL'. } - TInitFinalRec = packed record - InitProc : CodePointer; - FinalProc: CodePointer; - end; - - TInitFinalTable = packed record - TableCount : longint; - Procs : array[1..1] of TInitFinalRec; { open array, actual size = TableCount } - end; - PInitFinalTable = ^TInitFinalTable; - - { Procedure type for calling initialization procs from the INITFINAL table } - TProcedure = procedure; var { Compiler-generated INITFINAL table — contains init/finalize procs for diff --git a/src/arch/x86_64/arch.x86_64.boot.pas b/src/arch/x86_64/arch.x86_64.boot.pas new file mode 100644 index 00000000..4012b9de --- /dev/null +++ b/src/arch/x86_64/arch.x86_64.boot.pas @@ -0,0 +1,83 @@ +// Copyright 2026 Aaron Hance +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +unit arch.x86_64.boot; + +interface + +procedure kmain(info: pointer; magic: uint32); cdecl; + +implementation + +uses io.stdio, core.strings, driver.storage.fdtable, arch.x86_64.proc.sched, proc.mgr, arch.x86_64.context, arch.x86_64.irq, memory.heap, debug.early, arch.x86_64.debug, arch.x86_64.isr, boot.multiboot, + arch.x86_64.memory.physical, arch.x86_64.memory.virtual; + +function long_mode_active: boolean; assembler; nostackframe; +asm + mov ecx, $C0000080 + rdmsr + shr eax, 10 + and eax, 1 +end; + +procedure kmain(info: pointer; magic: uint32); cdecl; [public, alias: 'kmain']; +var + wide: PtrUInt; +begin + arch.x86_64.debug.init; + arch.x86_64.isr.init; + puts('ASURO x86_64: entered Pascal'#13#10); + if (magic <> $2BADB002) or (info = nil) then begin + puts('FAIL: Multiboot handoff'#13#10); + bootstrap_halt; + end; + if (sizeof(pointer) <> 8) or (sizeof(PtrUInt) <> 8) or + (sizeof(SizeUInt) <> 8) or (sizeof(uint32) <> 4) then begin + puts('FAIL: native type widths'#13#10); + bootstrap_halt; + end; + wide := PtrUInt($100000000) + PtrUInt(magic); + if (wide shr 32 <> 1) or not long_mode_active then begin + puts('FAIL: long mode or 64-bit arithmetic'#13#10); + bootstrap_halt; + end; + puts('PASS: long mode, Multiboot handoff, 64-bit Pascal types'#13#10); + arch.x86_64.isr.UnitTest; + arch.x86_64.memory.physical.init(Pmultiboot_info_t(info)); + arch.x86_64.memory.physical.UnitTest; + arch.x86_64.memory.virtual.init; + arch.x86_64.memory.virtual.UnitTest; + memory.heap.init; + memory.heap.UnitTest; + core.strings.UnitTest; + io.stdio.init; + arch.x86_64.irq.init; + arch.x86_64.irq.UnitTest; + arch.x86_64.context.UnitTest; + driver.storage.fdtable.UnitTest; + proc.mgr.init; + arch.x86_64.proc.sched.init; + arch.x86_64.proc.sched.UnitTest; + {$ifdef ASURO_TEST_UD} + arch.x86_64.isr.testFatal(6); + {$endif} + {$ifdef ASURO_TEST_PF} + arch.x86_64.isr.testFatal(14); + {$endif} + {$ifdef ASURO_TEST_DF} + arch.x86_64.isr.testFatal(8); + {$endif} + puts('ASURO x86_64: bootstrap complete; interrupts disabled'#13#10); + bootstrap_halt; +end; + +end. diff --git a/src/arch/x86_64/arch.x86_64.context.pas b/src/arch/x86_64/arch.x86_64.context.pas new file mode 100644 index 00000000..309d0986 --- /dev/null +++ b/src/arch/x86_64/arch.x86_64.context.pas @@ -0,0 +1,156 @@ +// Copyright 2026 Aaron Hance +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +{ Architecture context mechanism only. Scheduling policy stays in proc.*. + A saved context points at an aligned 512-byte FX image followed by the + normalized interrupt-frame pointer. Only kernel contexts are supported. } +unit arch.x86_64.context; +interface +type + PNative = ^PtrUInt; + { Exact order produced by isr/entry.asm. Long mode always pushes SS/RSP, + even without a privilege transition; only the error code is conditional. } + PFrame = ^TFrame; + TFrame = packed record + cr2: uint64; + r15, r14, r13, r12, r11, r10, r9, r8: uint64; + rdi, rsi, rbp, rbx, rdx, rcx, rax: uint64; + vector, error, rip, cs, flags, rsp, ss: uint64; + end; + + TSelectContext = function(saved: PtrUInt): PtrUInt; +procedure setSelector(selector: TSelectContext); +function prepare(stack: pointer; bytes: SizeUInt; entry: pointer; argument: PtrUInt): PtrUInt; +procedure UnitTest; +implementation +uses proc.types, proc.scheduler, arch.x86_64.util, arch.x86_64.irq, arch.common.legacyirq, memory.heap, debug.early; +var + choose: TSelectContext; + testProcesses: array[0..2] of PProcessContext; + testCurrent: PProcessContext; + testCursor: uint32; + testDone: array[0..1] of uint32; + testFailures: uint32; + testSwitches: uint32; +procedure setSelector(selector: TSelectContext); +begin + { Caller owns IRQ exclusion and context lifetimes. } + choose := selector; +end; +function selectContext(saved: PtrUInt): PtrUInt; cdecl; [public, alias: 'x64_select_context']; +var frame: PFrame; +begin + selectContext := saved; + frame := PFrame(PNative(saved + 512)^); + if (frame^.vector = 32) and assigned(choose) then + selectContext := choose(saved); +end; +function prepare(stack: pointer; bytes: SizeUInt; entry: pointer; argument: PtrUInt): PtrUInt; +var top, frameAddress, fx: PtrUInt; + frame: PFrame; +begin + prepare := 0; + if (stack = nil) or (entry = nil) or (bytes < 2048) or + (PtrUInt(stack) > High(PtrUInt) - bytes) then exit; + top := (PtrUInt(stack) + bytes) and not PtrUInt(15); + { SysV entry has RSP mod 16 = 8, as if CALL pushed a return address. } + top := top - 8; + PNative(top)^ := PtrUInt(@bootstrap_halt); + frameAddress := top - sizeof(TFrame); + fx := (frameAddress - 528) and not PtrUInt(15); + zero(pointer(fx), top - fx); + frame := PFrame(frameAddress); + frame^.rdi := argument; + frame^.rip := PtrUInt(entry); + frame^.cs := $18; frame^.ss := $10; + frame^.flags := $202; frame^.rsp := top; + PUInt16(fx)^ := $037F; { initial x87 control word; empty tag word } + PUInt32(fx + 24)^ := $1F80; { initial MXCSR } + PNative(fx + 512)^ := frameAddress; + prepare := fx; +end; +function testGetProcess(index: uint32): PProcessContext; +begin + if index < 3 then testGetProcess := testProcesses[index] else testGetProcess := nil; +end; +function testSelect(saved: PtrUInt): PtrUInt; +begin + testCurrent^.SavedStack := saved; + testCurrent := proc.scheduler.pickNext(testCurrent, testProcesses[0], 3, + testCursor, @testGetProcess); + inc(testSwitches); + testSelect := testCurrent^.SavedStack; +end; +function testRegisters(counter: PUInt32; pattern: uint64): uint32; cdecl; external name 'x64_test_context_registers'; +procedure worker(index: PtrUInt); cdecl; +var value: PtrUInt; + allocation: void; +begin + value := PtrUInt($1234567800000000) + index; + allocation := memory.heap.kalloc(17 + index); + PUInt64(allocation)^ := value; + if testRegisters(@testSwitches, value) <> 1 then inc(testFailures); + if (PUInt64(allocation)^ <> value) or + (value <> PtrUInt($1234567800000000) + index) then inc(testFailures); + memory.heap.kfree(allocation); + asm cli end; + testProcesses[index + 1]^.State := psFinished; + PUInt32(@testDone[index])^ := 1; + asm sti end; + { The next timer moves away permanently; stack is freed by bootstrap. } + while true do asm pause end; +end; +procedure UnitTest; +var stackA, stackB: void; + beforeFree: SizeUInt; + attempts, i: uint32; + saved: PtrUInt; +begin + beforeFree := lmm_total_free; + stackA := kalloc(16384); stackB := kalloc(16384); + for i := 0 to 2 do begin + testProcesses[i] := PProcessContext(kalloc(sizeof(TProcessContext))); + testProcesses[i]^.ProcessID := i; + testProcesses[i]^.Quantum := 1; + testProcesses[i]^.State := psReady; + end; + testProcesses[1]^.SavedStack := prepare(pointer(stackA), 16384, @worker, 0); + testProcesses[2]^.SavedStack := prepare(pointer(stackB), 16384, @worker, 1); + if (testProcesses[1]^.SavedStack = 0) or (testProcesses[2]^.SavedStack = 0) then begin + puts('FAIL: prepare kernel contexts'#13#10); bootstrap_halt; + end; + testCurrent := testProcesses[0]; testCurrent^.State := psRunning; testCursor := 0; testDone[0] := 0; testDone[1] := 0; + testFailures := 0; testSwitches := 0; + setSelector(@testSelect); + masks($FE, $FF); + asm sti end; + attempts := 0; + while ((PUInt32(@testDone[0])^ = 0) or (PUInt32(@testDone[1])^ = 0)) and + (attempts < 10000000) do begin + inc(attempts); asm pause end; + end; + saved := lockIRQ; + masks($FF, $FF); + setSelector(nil); + if (testDone[0] = 0) or (testDone[1] = 0) or (testSwitches < 6) or + ((saved and $200) = 0) then inc(testFailures); + kfree(stackA); kfree(stackB); + for i := 0 to 2 do begin kfree(void(testProcesses[i])); testProcesses[i] := nil; end; + if lmm_total_free <> beforeFree then inc(testFailures); + field('CONTEXT_SWITCHES', testSwitches); + field('CONTEXT_TESTS_FAILED', testFailures); + if testFailures <> 0 then begin + puts('FAIL: preemptive kernel contexts'#13#10); bootstrap_halt; + end; + puts('PASS: two kernel contexts preserve registers/x87/SSE, allocate and release stacks'#13#10); +end; +end. diff --git a/src/arch/x86_64/arch.x86_64.debug.pas b/src/arch/x86_64/arch.x86_64.debug.pas new file mode 100644 index 00000000..6edfd396 --- /dev/null +++ b/src/arch/x86_64/arch.x86_64.debug.pas @@ -0,0 +1,39 @@ +// Copyright 2026 Aaron Hance +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +unit arch.x86_64.debug; +interface +procedure init; +implementation +uses debug.early; +{ The stub initializes COM1. Bounded polling also allows boot without a UART. } +procedure putchar(ch: char); assembler; nostackframe; +asm + mov ecx, 65536 + mov dx, $3FD +@wait: + in al, dx + test al, $20 + jnz @ready + loop @wait + ret +@ready: + mov eax, edi + mov dx, $3F8 + out dx, al +end; + +procedure init; +begin + debug.early.setWriter(@putchar); +end; +end. diff --git a/src/arch/x86_64/arch.x86_64.irq.pas b/src/arch/x86_64/arch.x86_64.irq.pas new file mode 100644 index 00000000..68d504f8 --- /dev/null +++ b/src/arch/x86_64/arch.x86_64.irq.pas @@ -0,0 +1,91 @@ +// Copyright 2026 Aaron Hance +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +unit arch.x86_64.irq; +interface +procedure init; +procedure dispatch(vector: uint32); +procedure UnitTest; +function ticks: uint64; +implementation +uses arch.common.legacyirq, arch.x86_64.util, debug.early, memory.heap; +var + timerTicks, unexpected, spurious: uint64; + ready: boolean; +procedure init; +begin + asm cli end; + remap($FF, $FF); + timerTicks := 0; unexpected := 0; spurious := 0; + timerStart; + ready := true; +end; +function ticks: uint64; +begin + ticks := PUInt64(@timerTicks)^; +end; +procedure dispatch(vector: uint32); +var line: uint8; +begin + if (not ready) or (vector < 32) or (vector > 47) then begin + puts('FAIL: IRQ dispatched before controller setup'#13#10); bootstrap_halt; + end; + line := vector - 32; + if not realIRQ(line) then begin inc(spurious); exit; end; + if line = 0 then inc(timerTicks) else inc(unexpected); + acknowledge(line); +end; +procedure UnitTest; +var + attempts, failed: uint32; + saved, beforeTicks: uint64; + allocation: void; + procedure Assert(condition: boolean; name: pchar); + begin + if condition then puts('PASS: ') else begin inc(failed); puts('FAIL: '); end; + puts(name); puts(#13#10); + end; +begin + failed := 0; + { Synthetic spurious vectors must return without delivering device work. } + asm int $27; int $2F end; + Assert((spurious = 2) and (unexpected = 0), 'PIC spurious IRQ7/IRQ15 filtered'); + masks($FE, $FF); { only IRQ0; no storage or keyboard IRQs yet } + beforeTicks := ticks; + asm sti end; + attempts := 0; + { Bounded polling, not HLT: a missing timer must report failure. } + while (ticks - beforeTicks < 16) and (attempts < 10000000) do begin + asm pause end; + inc(attempts); + end; + saved := lockIRQ; + Assert((saved and $200) <> 0, 'IRQ return restores interrupt enable'); + Assert(ticks - beforeTicks >= 16, 'PIT delivers repeated hardware interrupts'); + beforeTicks := ticks; + for attempts := 1 to 10000 do asm pause end; + Assert(ticks = beforeTicks, 'CLI excludes timer interrupts'); + unlockIRQ(saved); + allocation := memory.heap.kalloc(17); + saved := lockIRQ; + Assert((saved and $200) <> 0, 'heap allocation preserves enabled interrupts'); + unlockIRQ(saved); + memory.heap.kfree(allocation); + saved := lockIRQ; + Assert((saved and $200) <> 0, 'heap release preserves enabled interrupts'); + masks($FF, $FF); + Assert(unexpected = 0, 'no unexpected hardware IRQs'); + field('IRQ_TIMER_TICKS', ticks); + field('IRQ_TESTS_FAILED', failed); + if failed <> 0 then bootstrap_halt; +end; +end. diff --git a/src/arch/x86_64/arch.x86_64.isr.pas b/src/arch/x86_64/arch.x86_64.isr.pas new file mode 100644 index 00000000..1bd40d91 --- /dev/null +++ b/src/arch/x86_64/arch.x86_64.isr.pas @@ -0,0 +1,245 @@ +// Copyright 2026 Aaron Hance +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +unit arch.x86_64.isr; + +interface + +procedure init; +procedure UnitTest; +procedure testFatal(vector: uint32); + +implementation + +uses debug.early, arch.x86_64.irq, arch.x86_64.context; + +type + TGate = packed record + offsetLow, selector: uint16; + ist, attributes: uint8; + offsetMiddle: uint16; + offsetHigh, reserved: uint32; + end; + TDescriptorPointer = packed record + limit: uint16; + base: uint64; + end; + TTSS = packed record + reserved0: uint32; + rsp0, rsp1, rsp2, reserved1: uint64; + ist: array[0..6] of uint64; + reserved2: uint64; + reserved3, ioMapBase: uint16; + end; + TTSSDescriptor = packed record + limitLow, baseLow: uint16; + baseMiddle, access, flags, baseHigh: uint8; + baseUpper, reserved: uint32; + end; + +var + { Hardware tables must exist before the heap. Fixed bounded reservations, + not per-fault allocations; GRUB clears their BSS storage. } + gates: array[0..255] of TGate; + idtr: TDescriptorPointer; + tss: TTSS; + tssDescriptor: TTSSDescriptor; external name 'bootstrap_tss_descriptor'; + entryTable: array[0..255] of uint64; external name 'x64_isr_table'; + kernelStackTop: uint8; external name 'stack_top'; + dfBottom: uint8; external name 'x64_df_stack_bottom'; + dfTop: uint8; external name 'x64_df_stack_top'; + nmiTop: uint8; external name 'x64_nmi_stack_top'; + mcTop: uint8; external name 'x64_mc_stack_top'; + breakpointReturn: uint8; external name 'x64_test_breakpoint_return'; + expectedRSP: uint64; external name 'x64_test_expected_rsp'; + pageFaultInstruction: uint8; external name 'x64_test_page_fault_instruction'; + pageFaultReturn: uint8; external name 'x64_test_page_fault_return'; + expectedTest: uint32; + breakpointValid, pageFaultValid, fatalActive: boolean; + +function testBreakpoint: uint32; cdecl; external name 'x64_test_breakpoint'; +procedure testPageFault; cdecl; external name 'x64_test_page_fault'; +procedure testInvalidOpcode; cdecl; external name 'x64_test_invalid_opcode'; +procedure testDoubleFault; cdecl; external name 'x64_test_double_fault'; + +procedure loadTables(descriptor: pointer); assembler; nostackframe; +asm + mov ax, $20 + ltr ax + lidt [rdi] +end; + +procedure init; +var + i: uint32; + address: uint64; +begin + if (sizeof(TGate) <> 16) or (sizeof(TDescriptorPointer) <> 10) or + (sizeof(TTSS) <> 104) or (sizeof(TTSSDescriptor) <> 16) or + (sizeof(TFrame) <> 184) then begin + puts('FAIL: x86_64 hardware table layout'#13#10); + bootstrap_halt; + end; + tss.rsp0 := PtrUInt(@kernelStackTop); + tss.ist[0] := PtrUInt(@dfTop); + tss.ist[1] := PtrUInt(@nmiTop); + tss.ist[2] := PtrUInt(@mcTop); + tss.ioMapBase := sizeof(TTSS); { beyond limit: no I/O permission bitmap } + address := PtrUInt(@tss); + tssDescriptor.limitLow := sizeof(TTSS) - 1; + tssDescriptor.baseLow := uint16(address); + tssDescriptor.baseMiddle := uint8(address shr 16); + tssDescriptor.access := $89; { present, DPL0, available 64-bit TSS } + tssDescriptor.flags := 0; + tssDescriptor.baseHigh := uint8(address shr 24); + tssDescriptor.baseUpper := uint32(address shr 32); + tssDescriptor.reserved := 0; + for i := 0 to 255 do begin + address := entryTable[i]; + gates[i].offsetLow := uint16(address); + gates[i].selector := $18; + gates[i].ist := 0; + gates[i].attributes := $8E; { present DPL0 interrupt gate, clears IF } + gates[i].offsetMiddle := uint16(address shr 16); + gates[i].offsetHigh := uint32(address shr 32); + gates[i].reserved := 0; + end; + gates[8].ist := 1; + gates[2].ist := 2; + gates[18].ist := 3; + idtr.limit := sizeof(gates) - 1; + idtr.base := PtrUInt(@gates); + loadTables(@idtr); + puts('ASURO x86_64: IDT/TSS ready; DF/NMI/MC emergency stacks installed'#13#10); +end; + +procedure dispatch(frame: PFrame); cdecl; [public, alias: 'x64_exception_dispatch']; +const + names: array[0..14] of pchar = ( + 'R15', 'R14', 'R13', 'R12', 'R11', 'R10', 'R9', 'R8', + 'RDI', 'RSI', 'RBP', 'RBX', 'RDX', 'RCX', 'RAX'); +var + i: uint32; + registers: PUInt64; +begin + if (frame^.vector >= 32) and (frame^.vector <= 47) then begin + arch.x86_64.irq.dispatch(frame^.vector); + exit; + end; + registers := @frame^.r15; + if (expectedTest = 1) and (frame^.vector = 3) and + (frame^.rip = PtrUInt(@breakpointReturn)) then begin + breakpointValid := (frame^.error = 0) and (frame^.cs = $18) and + (frame^.ss = $10) and (frame^.rsp = expectedRSP) and + ((frame^.flags and $401) = $401); + for i := 0 to 14 do + if registers[i] <> $1122334455667788 then breakpointValid := false; + { Deliberately destroy FX state inside the handler: the assembly return + path must restore it, not merely rely on Pascal avoiding SIMD. } + asm + fninit + pxor xmm0, xmm0 + pxor xmm15, xmm15 + end; + exit; + end; + if (expectedTest = 2) and (frame^.vector = 14) and + (frame^.rip = PtrUInt(@pageFaultInstruction)) then begin + pageFaultValid := (frame^.error = 2) and (frame^.cr2 = $100000000) and + (frame^.cs = $18) and (frame^.ss = $10); + frame^.rip := PtrUInt(@pageFaultReturn); { recover only this exact probe } + exit; + end; + + if fatalActive then begin + puts('FATAL: nested exception while reporting'#13#10); + field('VECTOR', frame^.vector); + bootstrap_halt; + end; + fatalActive := true; + puts('FATAL x86_64 exception: '); + case frame^.vector of + 2: puts('NMI'); + 6: puts('invalid opcode'); + 8: puts('double fault'); + 13: puts('general protection'); + 14: puts('page fault'); + 18: puts('machine check'); + else + puts('unexpected vector'); + end; + puts(#13#10); + field('VECTOR', frame^.vector); + field('ERROR', frame^.error); + field('RIP', frame^.rip); + field('CS', frame^.cs); + field('RFLAGS', frame^.flags); + field('RSP', frame^.rsp); + field('SS', frame^.ss); + field('CR2', frame^.cr2); + field('FRAME', PtrUInt(frame)); + if (PtrUInt(frame) >= PtrUInt(@dfBottom)) and + (PtrUInt(frame) + sizeof(TFrame) <= PtrUInt(@dfTop)) then + puts('STACK=double-fault IST1'#13#10); + for i := 0 to 14 do field(names[i], registers[i]); + puts('HALTED: exception diagnostics complete'#13#10); + bootstrap_halt; +end; + +procedure UnitTest; +var + passed, failed: uint32; + result: uint32; + procedure Assert(condition: boolean; name: pchar); + begin + if condition then begin + inc(passed); + puts('PASS: '); + end else begin + inc(failed); + puts('FAIL: '); + end; + puts(name); + puts(#13#10); + end; +begin + passed := 0; + failed := 0; + breakpointValid := false; + expectedTest := 1; + result := testBreakpoint; + expectedTest := 0; + Assert(breakpointValid and (result = 1), + 'INT3 frame, GPRs, RFLAGS, x87/SSE and IRETQ'); + pageFaultValid := false; + expectedTest := 2; + testPageFault; + expectedTest := 0; + Assert(pageFaultValid, 'page-fault error code, CR2 and IRETQ recovery'); + field('EXCEPTION_TESTS_PASSED', passed); + field('EXCEPTION_TESTS_FAILED', failed); + if failed <> 0 then bootstrap_halt; +end; + +procedure testFatal(vector: uint32); +begin + puts('TEST: deliberate fatal exception'#13#10); + case vector of + 6: testInvalidOpcode; + 14: testPageFault; + 8: testDoubleFault; + end; + puts('FAIL: fatal exception returned'#13#10); + bootstrap_halt; +end; + +end. diff --git a/src/arch/x86_64/arch.x86_64.util.pas b/src/arch/x86_64/arch.x86_64.util.pas new file mode 100644 index 00000000..13a35328 --- /dev/null +++ b/src/arch/x86_64/arch.x86_64.util.pas @@ -0,0 +1,67 @@ +// Copyright 2026 Aaron Hance +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +unit arch.x86_64.util; +interface +function lockIRQ: PtrUInt; +procedure unlockIRQ(flags: PtrUInt); +procedure zero(dest: pointer; bytes: SizeUInt); +procedure invalidate(address: PtrUInt); +function pageRoot: uint64; +procedure outb(port: uint16; value: uint8); +function inb(port: uint16): uint8; +implementation +function lockIRQ: PtrUInt; assembler; nostackframe; +asm + pushfq + cli + pop rax +end; +procedure unlockIRQ(flags: PtrUInt); assembler; nostackframe; +asm + test rdi, $200 + jz @done + sti +@done: +end; +procedure zero(dest: pointer; bytes: SizeUInt); assembler; nostackframe; +asm + mov rcx, rsi + shr rcx, 3 + xor eax, eax + cld + rep stosq + mov rcx, rsi + and rcx, 7 + rep stosb +end; +procedure invalidate(address: PtrUInt); assembler; nostackframe; +asm + invlpg [rdi] +end; +function pageRoot: uint64; assembler; nostackframe; +asm + mov rax, cr3 +end; +procedure outb(port: uint16; value: uint8); assembler; nostackframe; +asm + mov edx, edi + mov eax, esi + out dx, al +end; +function inb(port: uint16): uint8; assembler; nostackframe; +asm + mov edx, edi + xor eax, eax + in al, dx +end; +end. diff --git a/src/arch/x86_64/boot/stub.asm b/src/arch/x86_64/boot/stub.asm new file mode 100644 index 00000000..c7789395 --- /dev/null +++ b/src/arch/x86_64/boot/stub.asm @@ -0,0 +1,209 @@ +; Copyright 2026 Aaron Hance +; +; Licensed under the Apache License, Version 2.0 (the "License"); +; you may not use this file except in compliance with the License. +; You may obtain a copy of the License at +; http://www.apache.org/licenses/LICENSE-2.0 +; Unless required by applicable law or agreed to in writing, software +; distributed under the License is distributed on an "AS IS" BASIS, +; WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +; See the License for the specific language governing permissions and +; limitations under the License. + +bits 32 +global loader +global bootstrap_halt +global bootstrap_tss_descriptor +global stack_top +extern kmain +extern load_end +extern kernel_end + +; Explicit Multiboot v1 load addresses allow GRUB to load this ELF64 image +; while still handing off in 32-bit protected mode. All bootstrap addresses +; are below 4 GiB. The linker keeps this header first in the load image. +section .multiboot align=4 +header: + dd 0x1BADB002 + dd 0x00010003 + dd -(0x1BADB002 + 0x00010003) + dd header + dd header + dd load_end + dd kernel_end + dd loader + +section .text +loader: + cli + cld + ; Own flat descriptors before relying on the bootloader's data segments. + lgdt [gdt_pointer] + jmp 0x08:protected_entry +protected_entry: + mov dx, 0x10 + mov ds, dx + mov es, dx + mov ss, dx + mov esp, stack_top + mov esi, eax ; preserve Multiboot magic across CPUID + mov ebp, ebx ; physical Multiboot info pointer + call serial_init + cmp esi, 0x2BADB002 + jne fail_boot + + ; CPUID availability: toggle EFLAGS.ID and restore the original flags. + pushfd + pop eax + mov ecx, eax + xor eax, 1 << 21 + push eax + popfd + pushfd + pop eax + push ecx + popfd + xor eax, ecx + test eax, 1 << 21 + jz fail_cpu + mov eax, 0x80000000 + cpuid + cmp eax, 0x80000001 + jb fail_cpu + mov eax, 0x80000001 + cpuid + test edx, 1 << 29 + jz fail_cpu + + ; Reserve six bootstrap pages: PML4, PDPT, and four page directories. + ; Identity-map the first 4 GiB using 2 MiB leaves, including boot data. + mov edi, pml4 + xor eax, eax + mov ecx, (6 * 4096) / 4 + rep stosd + mov dword [pml4], pdpt + 3 + mov edi, pdpt + mov eax, page_directories + 3 + mov ecx, 4 +.pdpt: + mov [edi], eax + add edi, 8 + add eax, 4096 + loop .pdpt + mov edi, page_directories + mov eax, 0x83 + mov ecx, 2048 +.pages: + mov [edi], eax + add edi, 8 + add eax, 0x200000 + loop .pages + + ; Multiboot enters with paging off. PAE must precede activation of LME. + mov eax, cr4 + or eax, (1 << 5) | (1 << 9) | (1 << 10) + mov cr4, eax + mov eax, pml4 + mov cr3, eax + mov ecx, 0xC0000080 + rdmsr + or eax, 1 << 8 + wrmsr + mov eax, cr0 + and eax, ~((1 << 2) | (1 << 3)) ; clear EM/TS for compiler SSE2 use + or eax, (1 << 31) | (1 << 1) + mov cr0, eax + jmp 0x18:long_entry + +fail_boot: + mov bl, 'B' + jmp fail +fail_cpu: + mov bl, 'C' +fail: + mov ecx, 65536 + mov dx, 0x3FD +.wait: + in al, dx + test al, 0x20 + jnz .write + loop .wait + jmp .halt +.write: + mov dx, 0x3F8 + mov al, bl + out dx, al +.halt: + cli + hlt + jmp .halt + +serial_init: + mov dx, 0x3F9 + xor al, al + out dx, al + mov dx, 0x3FB + mov al, 0x80 + out dx, al + mov dx, 0x3F8 + mov al, 1 + out dx, al + mov dx, 0x3F9 + xor al, al + out dx, al + mov dx, 0x3FB + mov al, 3 + out dx, al + mov dx, 0x3FA + mov al, 0xC7 + out dx, al + mov dx, 0x3FC + mov al, 0x0B + out dx, al + ret + +bits 64 +long_entry: + mov ax, 0x10 + mov ds, ax + mov es, ax + mov ss, ax + xor eax, eax + mov fs, ax + mov gs, ax + mov rsp, stack_top + mov edi, ebp ; SysV AMD64: RDI=info, RSI=magic + xor ebp, ebp + fninit + ldmxcsr [rel initial_mxcsr] + call kmain +bootstrap_halt: + cli + hlt + jmp bootstrap_halt + +section .data align=8 +gdt: + dq 0 + dq 0x00CF9A000000FFFF ; 0x08: 32-bit flat code + dq 0x00CF92000000FFFF ; 0x10: flat data + dq 0x00AF9A000000FFFF ; 0x18: 64-bit code, L=1 D=0 +bootstrap_tss_descriptor: + dq 0, 0 ; 0x20: 16-byte TSS descriptor filled before LTR +gdt_end: +gdt_pointer: + dw gdt_end - gdt - 1 + dd gdt +section .rodata align=4 +initial_mxcsr: + dd 0x1F80 + +section .bss align=4096 +; Early boot has no allocator. These bounded reservations are owned by the +; bootstrap and must remain reserved when the physical allocator is introduced. +pml4: resb 4096 +pdpt: resb 4096 +page_directories: resb 4 * 4096 +align 16 +stack_bottom: resb 16384 +stack_top: diff --git a/src/arch/x86_64/isr/entry.asm b/src/arch/x86_64/isr/entry.asm new file mode 100644 index 00000000..b9ef362b --- /dev/null +++ b/src/arch/x86_64/isr/entry.asm @@ -0,0 +1,276 @@ +; Copyright 2026 Aaron Hance +; +; Licensed under the Apache License, Version 2.0 (the "License"); +; you may not use this file except in compliance with the License. +; You may obtain a copy of the License at +; http://www.apache.org/licenses/LICENSE-2.0 +; Unless required by applicable law or agreed to in writing, software +; distributed under the License is distributed on an "AS IS" BASIS, +; WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +; See the License for the specific language governing permissions and +; limitations under the License. + +bits 64 +extern x64_exception_dispatch +extern x64_select_context +extern bootstrap_halt + +global x64_isr_table +global x64_test_breakpoint +global x64_test_breakpoint_return +global x64_test_expected_rsp +global x64_test_page_fault +global x64_test_page_fault_instruction +global x64_test_page_fault_return +global x64_test_double_fault +global x64_test_invalid_opcode +global x64_df_stack_bottom, x64_df_stack_top +global x64_nmi_stack_top, x64_mc_stack_top + +section .text +; Normalize hardware error-code and no-error-code frames. Never use software +; INT to invoke vectors whose CPU-generated exceptions push an error code. +%assign vector 0 +%rep 256 +isr_%+vector: +%if vector != 8 && vector != 10 && vector != 11 && vector != 12 && vector != 13 && vector != 14 && vector != 17 && vector != 21 && vector != 29 && vector != 30 + push qword 0 +%endif + push qword vector + jmp common_entry +%assign vector vector+1 +%endrep + +common_entry: + push rax + push rcx + push rdx + push rbx + push rbp + push rsi + push rdi + push r8 + push r9 + push r10 + push r11 + push r12 + push r13 + push r14 + push r15 + mov rax, cr2 + push rax + ; All interrupted registers are saved before changing DF or calling Pascal. + cld + mov rbx, rsp + sub rsp, 528+15 + and rsp, -16 + fxsave64 [rsp] + mov [rsp+512], rbx + mov r12, rsp + mov rdi, rbx + call x64_exception_dispatch + mov rdi, r12 + call x64_select_context + fxrstor64 [rax] + mov rsp, [rax+512] + add rsp, 8 ; captured CR2 + pop r15 + pop r14 + pop r13 + pop r12 + pop r11 + pop r10 + pop r9 + pop r8 + pop rdi + pop rsi + pop rbp + pop rbx + pop rdx + pop rcx + pop rax + add rsp, 16 ; normalized vector and error code + iretq + +; Test the real entry/return path, including nonvolatile registers, high halves, +; DF/CF, x87, and XMM0/XMM15. Preserve the caller's original FX state as well. +x64_test_breakpoint: + push rbx + push rbp + push r12 + push r13 + push r14 + push r15 + sub rsp, 536 ; aligned 512-byte FX area + scratch + fxsave64 [rsp] + mov [rel x64_test_expected_rsp], rsp + fninit + fld1 + pcmpeqd xmm0, xmm0 + pcmpeqd xmm15, xmm15 +%macro SEED 1 + mov %1, 0x1122334455667788 +%endmacro + SEED rax + SEED rcx + SEED rdx + SEED rbx + SEED rbp + SEED rsi + SEED rdi + SEED r8 + SEED r9 + SEED r10 + SEED r11 + SEED r12 + SEED r13 + SEED r14 + SEED r15 + std + stc + int3 +x64_test_breakpoint_return: + pushfq + pop qword [rsp+512] + cld +%macro CHECK 1 + cmp %1, [rel expected_gpr] + jne .failed +%endmacro + CHECK rax + CHECK rcx + CHECK rdx + CHECK rbx + CHECK rbp + CHECK rsi + CHECK rdi + CHECK r8 + CHECK r9 + CHECK r10 + CHECK r11 + CHECK r12 + CHECK r13 + CHECK r14 + CHECK r15 + mov eax, [rsp+512] + and eax, 0x401 ; CF and DF must both survive IRETQ + cmp eax, 0x401 + jne .failed + pmovmskb eax, xmm0 + cmp eax, 0xFFFF + jne .failed + pmovmskb eax, xmm15 + cmp eax, 0xFFFF + jne .failed + fistp qword [rsp+512] + cmp qword [rsp+512], 1 + jne .failed + mov eax, 1 + jmp .done +.failed: + xor eax, eax +.done: + fxrstor64 [rsp] + add rsp, 536 + pop r15 + pop r14 + pop r13 + pop r12 + pop rbp + pop rbx + ret + +x64_test_page_fault: + mov rax, 0x100000000 ; canonical, just beyond the bootstrap mapping +x64_test_page_fault_instruction: + mov [rax], rax ; write fault: nonzero hardware error code (2) +x64_test_page_fault_return: + ret + +x64_test_invalid_opcode: + ud2 + jmp bootstrap_halt + +x64_test_double_fault: + ; Both ordinary #UD and #PF delivery need this unmapped stack. #DF must + ; switch to IST1 and report the original broken RSP without touching it. + mov rsp, 0x100001000 + ud2 + jmp bootstrap_halt + +section .rodata align=8 +expected_gpr: dq 0x1122334455667788 +x64_isr_table: +%assign vector 0 +%rep 256 + dq isr_%+vector +%assign vector vector+1 +%endrep + +section .bss align=16 +x64_test_expected_rsp: resq 1 +alignb 16 +; Bounded bootstrap reservations, before an allocator exists. Keep these +; reserved in the future allocator. NMI/MC cannot overwrite the #DF stack. +x64_df_stack_bottom: resb 16384 +x64_df_stack_top: + resb 8192 +x64_nmi_stack_top: + resb 8192 +x64_mc_stack_top: + +section .text +global x64_test_context_registers +; RDI: monotonically increasing switch counter; RSI: distinct 64-bit pattern. +; Preserve the caller's ABI/FX state while checking real timer preemption. +x64_test_context_registers: + push rbp + mov rbp, rsp + push r12 + push r13 + push r14 + sub rsp, 560 + and rsp, -16 + fxsave64 [rsp] + mov [rsp+520], rsi + mov r12, rsi + mov r13, rdi + mov r14d, [rdi] + add r14d, 9 + movq xmm15, rsi + punpcklqdq xmm15, xmm15 + mov eax, esi + inc eax + mov [rsp+512], eax + fninit + fild dword [rsp+512] +.wait: + pause + cmp [r13], r14d + jb .wait + xor eax, eax + cmp r12, [rsp+520] + jne .done + cmp rsi, [rsp+520] + jne .done + movq rdx, xmm15 + cmp rdx, [rsp+520] + jne .done + movdqa xmm0, xmm15 + psrldq xmm0, 8 + movq rdx, xmm0 + cmp rdx, [rsp+520] + jne .done + fistp dword [rsp+516] + mov edx, [rsp+512] + cmp edx, [rsp+516] + jne .done + mov eax, 1 +.done: + fxrstor64 [rsp] + lea rsp, [rbp-24] + pop r14 + pop r13 + pop r12 + pop rbp + ret diff --git a/src/arch/x86_64/memory/arch.x86_64.memory.physical.pas b/src/arch/x86_64/memory/arch.x86_64.memory.physical.pas new file mode 100644 index 00000000..ec656970 --- /dev/null +++ b/src/arch/x86_64/memory/arch.x86_64.memory.physical.pas @@ -0,0 +1,304 @@ +// Copyright 2026 Aaron Hance +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +{ 4 KiB physical frames. Metadata is carved once from verified low RAM before + the heap exists. Non-available memory-map entries always override available + entries. Only the BSP is active; IRQ exclusion is not an SMP lock. } +unit arch.x86_64.memory.physical; +interface +uses boot.multiboot; +const + FRAME_SIZE = 4096; + MAX_MANAGED = uint64(64) shl 30; +procedure init(info: Pmultiboot_info_t); +function alloc_page(lowOnly: boolean): uint64; +function alloc_high: uint64; +function free_page(address: uint64): boolean; +function reserve_range(address, bytes: uint64): boolean; +function free_count: uint64; +procedure UnitTest; +implementation +uses arch.x86_64.util, debug.early; +var + eligible, used: PUInt32; + frameCount, wordCount, hint: uint32; + freeFrames: uint64; + reservedEnd: uint64; + kernelEnd: uint8; external name 'kernel_end'; + mapStart, mapEnd: PtrUInt; + +procedure fail(message: pchar); +begin + puts('FAIL: PMM '); puts(message); puts(#13#10); bootstrap_halt; +end; +function regionEnd(base, bytes: uint64): uint64; +begin + if (base >= MAX_MANAGED) or (bytes > MAX_MANAGED - base) then + regionEnd := MAX_MANAGED + else regionEnd := base + bytes; +end; +function nextEntry(entry: Pmemory_map_t): Pmemory_map_t; +begin + nextEntry := Pmemory_map_t(PtrUInt(entry) + entry^.size + 4); +end; +procedure preserve(address, bytes: uint64); +var finish: uint64; +begin + if (address >= $100000000) or (bytes > $100000000 - address) then + fail('boot data outside initial mapping'); + finish := address + bytes; + if finish > reservedEnd then reservedEnd := finish; +end; +procedure setAvailable(base, bytes: uint64; available: boolean); +var first, last, i, mask: uint32; finish: uint64; +begin + finish := regionEnd(base, bytes); + if (base >= finish) or (base >= uint64(frameCount) * FRAME_SIZE) then exit; + if available then begin + first := (base + FRAME_SIZE - 1) div FRAME_SIZE; + last := finish div FRAME_SIZE; + end else begin + first := base div FRAME_SIZE; + last := (finish + FRAME_SIZE - 1) div FRAME_SIZE; + end; + if last > frameCount then last := frameCount; + if first >= last then exit; + for i := first to last - 1 do begin + mask := uint32(1) shl (i and 31); + if available then begin + if (eligible[i shr 5] and mask) = 0 then begin + eligible[i shr 5] := eligible[i shr 5] or mask; + inc(freeFrames); + end; + end else if (eligible[i shr 5] and mask) <> 0 then begin + eligible[i shr 5] := eligible[i shr 5] and not mask; + dec(freeFrames); + end; + end; +end; +function metadataAddress(bytes: uint64): uint64; +var e, other: Pmemory_map_t; candidate, finish, nextCandidate: uint64; +begin + metadataAddress := 0; + e := Pmemory_map_t(mapStart); + while PtrUInt(e) < mapEnd do begin + if (e^.mtype = 1) and (e^.base_addr < $100000000) and (e^.length <> 0) then begin + candidate := (e^.base_addr + FRAME_SIZE - 1) and not uint64(FRAME_SIZE - 1); + if candidate < reservedEnd then candidate := reservedEnd; + finish := regionEnd(e^.base_addr, e^.length); + if finish > $100000000 then finish := $100000000; + while (candidate < finish) and (bytes <= finish - candidate) do begin + nextCandidate := candidate; + other := Pmemory_map_t(mapStart); + while PtrUInt(other) < mapEnd do begin + if (other^.mtype <> 1) and (other^.base_addr < candidate + bytes) and + (regionEnd(other^.base_addr, other^.length) > candidate) then begin + nextCandidate := (regionEnd(other^.base_addr, other^.length) + FRAME_SIZE - 1) and not uint64(FRAME_SIZE - 1); + break; + end; + other := nextEntry(other); + end; + if nextCandidate = candidate then begin + metadataAddress := candidate; + exit; + end; + candidate := nextCandidate; { strictly advances across a reserved interval } + end; + end; + e := nextEntry(e); + end; +end; +procedure init(info: Pmultiboot_info_t); +var e: Pmemory_map_t; limit, bytes, storage: uint64; i, entries: uint32; + modules: Pmodule_t; sections: PtrUInt; sectionAddress, sectionBytes: uint64; +begin + if (info = nil) or ((info^.flags and (1 shl 6)) = 0) then fail('memory map missing'); + if (info^.mmap_length = 0) or (info^.mmap_length > $100000) or + (uint64(info^.mmap_addr) + info^.mmap_length > $100000000) then fail('invalid map length'); + mapStart := info^.mmap_addr; + mapEnd := mapStart + info^.mmap_length; + limit := 0; + entries := 0; + e := Pmemory_map_t(mapStart); + while PtrUInt(e) < mapEnd do begin + if (mapEnd - PtrUInt(e) < 24) or (e^.size < 20) or + (uint64(e^.size) + 4 > mapEnd - PtrUInt(e)) then fail('malformed map entry'); + inc(entries); + if entries > 4096 then fail('too many map entries'); + if (e^.mtype = 1) and (e^.length <> 0) and (e^.base_addr < MAX_MANAGED) then + if regionEnd(e^.base_addr, e^.length) > limit then limit := regionEnd(e^.base_addr, e^.length); + e := nextEntry(e); + end; + frameCount := limit div FRAME_SIZE; + if frameCount = 0 then fail('no usable RAM'); + reservedEnd := $1000000; { legacy low-memory devices and firmware } + preserve(0, PtrUInt(@kernelEnd)); + preserve(PtrUInt(info), 4096); + preserve(mapStart, info^.mmap_length); + if (info^.flags and (1 shl 2)) <> 0 then preserve(info^.cmdline, 4096); + if (info^.flags and (1 shl 9)) <> 0 then preserve(info^.boot_loader_name, 4096); + if (info^.flags and (1 shl 3)) <> 0 then begin + if info^.mods_count > 4096 then fail('too many boot modules'); + preserve(info^.mods_addr, uint64(info^.mods_count) * sizeof(module_t)); + modules := Pmodule_t(PtrUInt(info^.mods_addr)); + if info^.mods_count > 0 then for i := 0 to info^.mods_count - 1 do begin + if modules[i].mod_end < modules[i].mod_start then fail('invalid module range'); + preserve(modules[i].mod_start, modules[i].mod_end - modules[i].mod_start); + if modules[i].name <> 0 then preserve(modules[i].name, 4096); + end; + end; + if (info^.flags and (1 shl 4)) <> 0 then + preserve(info^.elf_sec.addr, uint64(info^.elf_sec.num) + info^.elf_sec.size); + if (info^.flags and (1 shl 5)) <> 0 then begin + if (info^.elf_sec.num > 4096) or + ((info^.elf_sec.size <> 40) and (info^.elf_sec.size <> 64)) then fail('invalid ELF sections'); + preserve(info^.elf_sec.addr, uint64(info^.elf_sec.num) * info^.elf_sec.size); + sections := info^.elf_sec.addr; + if info^.elf_sec.num > 0 then for i := 0 to info^.elf_sec.num - 1 do begin + if info^.elf_sec.size = 64 then begin + sectionAddress := PUInt64(sections + 16)^; + sectionBytes := PUInt64(sections + 32)^; + end else begin + sectionAddress := PUInt32(sections + 12)^; + sectionBytes := PUInt32(sections + 20)^; + end; + if (sectionAddress <> 0) and (sectionBytes <> 0) then preserve(sectionAddress, sectionBytes); + inc(sections, info^.elf_sec.size); + end; + end; + reservedEnd := (reservedEnd + FRAME_SIZE - 1) and not uint64(FRAME_SIZE - 1); + wordCount := (frameCount + 31) div 32; + bytes := (uint64(wordCount) * 8 + FRAME_SIZE - 1) and not uint64(FRAME_SIZE - 1); + storage := metadataAddress(bytes); + if storage = 0 then fail('no low RAM for frame metadata'); + eligible := PUInt32(PtrUInt(storage)); + used := PUInt32(PtrUInt(storage + uint64(wordCount) * 4)); + zero(eligible, bytes); + freeFrames := 0; + e := Pmemory_map_t(mapStart); + while PtrUInt(e) < mapEnd do begin + if e^.mtype = 1 then setAvailable(e^.base_addr, e^.length, true); + e := nextEntry(e); + end; + e := Pmemory_map_t(mapStart); + while PtrUInt(e) < mapEnd do begin + if e^.mtype <> 1 then setAvailable(e^.base_addr, e^.length, false); + e := nextEntry(e); + end; + setAvailable(0, reservedEnd, false); + setAvailable(storage, bytes, false); + hint := 0; + field('PMM_FREE_FRAMES', freeFrames); + field('PMM_MANAGED_LIMIT', uint64(frameCount) * FRAME_SIZE); + field('PMM_METADATA', storage); +end; +function allocateBetween(first, last: uint32): uint64; +var saved: PtrUInt; pass, start, finish, idx, bits, mask, bit: uint32; +begin + allocateBetween := 0; + saved := lockIRQ; + if last > frameCount then last := frameCount; + if first < last then begin + start := hint; + if (start < first) or (start >= last) then start := first; + finish := last; + for pass := 0 to 1 do begin + idx := start; + while idx < finish do begin + bits := eligible[idx shr 5] and not used[idx shr 5]; + bits := bits and ($FFFFFFFF shl (idx and 31)); + if bits <> 0 then begin + bit := 0; + while (bits and (uint32(1) shl bit)) = 0 do inc(bit); + idx := (idx and not uint32(31)) + bit; + if idx >= finish then break; + mask := uint32(1) shl bit; + used[idx shr 5] := used[idx shr 5] or mask; + dec(freeFrames); + hint := idx + 1; + allocateBetween := uint64(idx) * FRAME_SIZE; + unlockIRQ(saved); + exit; + end; + idx := (idx and not uint32(31)) + 32; + end; + finish := start; + start := first; + end; + end; + unlockIRQ(saved); +end; +function alloc_page(lowOnly: boolean): uint64; +begin + if lowOnly then alloc_page := allocateBetween(0, $100000) + else alloc_page := allocateBetween(0, frameCount); +end; +function alloc_high: uint64; +begin + alloc_high := allocateBetween($100000, frameCount); +end; +function free_page(address: uint64): boolean; +var idx, mask: uint32; saved: PtrUInt; +begin + free_page := false; + if (address and (FRAME_SIZE - 1) <> 0) or (address div FRAME_SIZE >= frameCount) then exit; + saved := lockIRQ; + idx := address div FRAME_SIZE; + mask := uint32(1) shl (idx and 31); + if ((eligible[idx shr 5] and used[idx shr 5]) and mask) <> 0 then begin + used[idx shr 5] := used[idx shr 5] and not mask; + inc(freeFrames); + if idx < hint then hint := idx; + free_page := true; + end; + unlockIRQ(saved); +end; +function reserve_range(address, bytes: uint64): boolean; +var first, last, i: uint32; saved: PtrUInt; +begin + reserve_range := false; + if (bytes = 0) or (address >= MAX_MANAGED) or (bytes > MAX_MANAGED - address) then exit; + first := address div FRAME_SIZE; + last := (address + bytes + FRAME_SIZE - 1) div FRAME_SIZE; + if last > frameCount then last := frameCount; + saved := lockIRQ; + if first < last then for i := first to last - 1 do + if (used[i shr 5] and (uint32(1) shl (i and 31))) <> 0 then begin + unlockIRQ(saved); exit; + end; + setAvailable(address, bytes, false); + reserve_range := true; + unlockIRQ(saved); +end; +function free_count: uint64; +begin free_count := freeFrames; end; +procedure UnitTest; +var before, a, b: uint64; passed, failed: uint32; + procedure Assert(condition: boolean; name: pchar); + begin + if condition then begin inc(passed); puts('PASS: '); end + else begin inc(failed); puts('FAIL: '); end; + puts(name); puts(#13#10); + end; +begin + passed := 0; failed := 0; before := free_count; + a := alloc_page(true); b := alloc_page(true); + Assert((a <> 0) and (b <> 0) and (a <> b) and (a >= reservedEnd) and + (b >= reservedEnd), 'PMM unique usable frames exclude boot reservations'); + Assert(not free_page(0) and not free_page(a + 1), 'PMM rejects reserved/unaligned frees'); + Assert(free_page(a) and free_page(b), 'PMM releases owned frames'); + Assert(not free_page(a), 'PMM rejects double free'); + Assert(free_count = before, 'PMM free-frame accounting restored'); + field('PMM_TESTS_FAILED', failed); + if failed <> 0 then bootstrap_halt; +end; +end. diff --git a/src/arch/x86_64/memory/arch.x86_64.memory.virtual.pas b/src/arch/x86_64/memory/arch.x86_64.memory.virtual.pas new file mode 100644 index 00000000..7f1b199e --- /dev/null +++ b/src/arch/x86_64/memory/arch.x86_64.memory.virtual.pas @@ -0,0 +1,310 @@ +// Copyright 2026 Aaron Hance +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +{ BSP kernel mappings. Page-table frames stay below 4 GiB so the bootstrap + identity map can access them; data frames may live above 4 GiB. } +unit arch.x86_64.memory.virtual; +interface +const + ARENA_SIZE = $400000; + ARENA_COUNT = 4096; + HEAP_BASE = PtrUInt($FFFF800000000000); +function allocate_region(arenas: uint32): PtrUInt; +function release_region(address: PtrUInt; arenas: uint32): boolean; +function map_page(address: PtrUInt; physical: uint64; owned: boolean): boolean; +function unmap_page(address: PtrUInt; var physical: uint64): boolean; +function translate(address: PtrUInt): uint64; +procedure init; +procedure UnitTest; +implementation +uses arch.x86_64.memory.physical, arch.x86_64.util, memory.bitmap, debug.early; +const + ADDRESS_MASK = uint64($000FFFFFFFFFF000); + PTE_OWNED = uint64(1) shl 9; +var + root: PUInt64; + arenaBitmap: PUInt32; + tablePages: uint64; + leafFlags: uint64; + failAfter: sint32; { deterministic OOM injection, disabled outside UnitTest } + +function acquire(lowOnly: boolean): uint64; +begin + acquire := 0; + if failAfter = 0 then exit; + if failAfter > 0 then dec(failAfter); + acquire := arch.x86_64.memory.physical.alloc_page(lowOnly); +end; +procedure fail(message: pchar); +begin puts('FAIL: VMM '); puts(message); puts(#13#10); bootstrap_halt; end; +function managed(address: PtrUInt): boolean; +begin + managed := (address >= HEAP_BASE) and (address - HEAP_BASE < uint64(ARENA_COUNT) * ARENA_SIZE); +end; +function enableNX: boolean; assembler; nostackframe; +asm + push rbx + mov eax, $80000001 + cpuid + xor eax, eax + test edx, 1 shl 20 + jz @done + mov ecx, $C0000080 + rdmsr + or eax, 1 shl 11 + wrmsr + mov eax, 1 +@done: + pop rbx +end; +function map_page(address: PtrUInt; physical: uint64; owned: boolean): boolean; +var table, slot: PUInt64; shift, created: sint32; frame, saved: uint64; + parents: array[0..2] of PUInt64; + frames: array[0..2] of uint64; +begin + map_page := false; + if not managed(address) or ((address and $FFF) <> 0) or + ((physical and not ADDRESS_MASK) <> 0) then exit; + saved := lockIRQ; + table := root; created := 0; shift := 39; + while shift > 12 do begin + slot := @table[(address shr shift) and $1FF]; + if (slot^ and 1) = 0 then begin + frame := acquire(true); + if frame = 0 then break; + zero(pointer(PtrUInt(frame)), 4096); + parents[created] := slot; + frames[created] := frame; + inc(created); inc(tablePages); + slot^ := frame or 3; + end; + if (slot^ and $80) <> 0 then break; + table := PUInt64(PtrUInt(slot^ and ADDRESS_MASK)); + dec(shift, 9); + end; + if shift = 12 then begin + slot := @table[(address shr 12) and $1FF]; + if (slot^ and 1) = 0 then begin + slot^ := physical or leafFlags; + if owned then slot^ := slot^ or PTE_OWNED; + invalidate(address); + map_page := true; + end; + end; + if not map_page then while created > 0 do begin + dec(created); + parents[created]^ := 0; + if not arch.x86_64.memory.physical.free_page(frames[created]) then fail('table rollback'); + dec(tablePages); + end; + unlockIRQ(saved); +end; +function leaf(address: PtrUInt): PUInt64; +var table: PUInt64; shift: sint32; entry: uint64; +begin + leaf := nil; + if not managed(address) then exit; + table := root; shift := 39; + while shift > 12 do begin + entry := table[(address shr shift) and $1FF]; + if ((entry and 1) = 0) or ((entry and $80) <> 0) then exit; + table := PUInt64(PtrUInt(entry and ADDRESS_MASK)); + dec(shift, 9); + end; + leaf := @table[(address shr 12) and $1FF]; +end; +function translate(address: PtrUInt): uint64; +var table: PUInt64; shift: sint32; entry, span: uint64; +begin + translate := 0; + if (address > $00007FFFFFFFFFFF) and (address < HEAP_BASE) then exit; + table := root; shift := 39; + while shift >= 12 do begin + entry := table[(address shr shift) and $1FF]; + if (entry and 1) = 0 then exit; + if (shift = 12) or (((entry and $80) <> 0) and (shift < 39)) then begin + span := uint64(1) shl shift; + translate := (entry and ADDRESS_MASK and not (span - 1)) + (address and (span - 1)); + exit; + end; + table := PUInt64(PtrUInt(entry and ADDRESS_MASK)); + dec(shift, 9); + end; +end; +procedure prune(address: PtrUInt); +var parents: array[0..2] of PUInt64; tables: array[0..2] of PUInt64; + table, slot: PUInt64; shift, level: sint32; i: uint32; empty: boolean; +begin + table := root; shift := 39; level := 0; + while shift > 12 do begin + slot := @table[(address shr shift) and $1FF]; + if ((slot^ and 1) = 0) or ((slot^ and $80) <> 0) then exit; + parents[level] := slot; + table := PUInt64(PtrUInt(slot^ and ADDRESS_MASK)); + tables[level] := table; + inc(level); dec(shift, 9); + end; + while level > 0 do begin + dec(level); empty := true; + for i := 0 to 511 do if (tables[level][i] and 1) <> 0 then begin empty := false; break; end; + if not empty then exit; + parents[level]^ := 0; + if not arch.x86_64.memory.physical.free_page(PtrUInt(tables[level])) then fail('table release'); + dec(tablePages); + end; +end; +function unmap_page(address: PtrUInt; var physical: uint64): boolean; +var slot: PUInt64; saved: PtrUInt; +begin + unmap_page := false; physical := 0; + if not managed(address) or ((address and $FFF) <> 0) then exit; + saved := lockIRQ; slot := leaf(address); + if slot <> nil then if (slot^ and 1) <> 0 then begin + physical := slot^ and ADDRESS_MASK; + slot^ := 0; + invalidate(address); + prune(address); + unmap_page := true; + end; + unlockIRQ(saved); +end; +procedure releaseMapped(address: PtrUInt; pages: uint32); +var i: uint32; slot: PUInt64; physical: uint64; +begin + if pages = 0 then exit; + for i := 0 to pages - 1 do begin + slot := leaf(address + uint64(i) * 4096); + if slot = nil then fail('missing owned mapping'); + physical := slot^ and ADDRESS_MASK; + slot^ := 0; + invalidate(address + uint64(i) * 4096); + if not arch.x86_64.memory.physical.free_page(physical) then fail('data frame release'); + end; + { Scan empty tables once per 2 MiB span, not once per 4 KiB page. } + i := 0; + while i < pages do begin + prune(address + uint64(i) * 4096); + inc(i, 512); + end; +end; +function allocate_region(arenas: uint32): PtrUInt; +var idx: sint32; i, pages: uint32; base, saved: PtrUInt; physical: uint64; +begin + allocate_region := 0; + if (arenas = 0) or (arenas > ARENA_COUNT) or (arenaBitmap = nil) then exit; + saved := lockIRQ; + idx := find_free_run(arenaBitmap, 0, arenas, ARENA_COUNT); + if idx < 0 then begin unlockIRQ(saved); exit; end; + bitmap_set_range(arenaBitmap, idx, arenas); + base := HEAP_BASE + uint64(idx) * ARENA_SIZE; + pages := arenas * (ARENA_SIZE div 4096); + i := 0; + while i < pages do begin + physical := acquire(false); + if physical = 0 then break; + if not map_page(base + uint64(i) * 4096, physical, true) then begin + if not arch.x86_64.memory.physical.free_page(physical) then fail('allocation rollback'); + break; + end; + inc(i); + end; + if i <> pages then begin + releaseMapped(base, i); + bitmap_clear_range(arenaBitmap, idx, arenas); + end else allocate_region := base; + unlockIRQ(saved); +end; +function release_region(address: PtrUInt; arenas: uint32): boolean; +var idx, i, pages: uint32; saved: PtrUInt; slot: PUInt64; +begin + release_region := false; + if not managed(address) or ((address and (ARENA_SIZE - 1)) <> 0) or + (arenas = 0) or (arenas > ARENA_COUNT) then exit; + idx := (address - HEAP_BASE) div ARENA_SIZE; + if arenas > ARENA_COUNT - idx then exit; + saved := lockIRQ; + if not bitmap_all_set(arenaBitmap, idx, arenas) then begin unlockIRQ(saved); exit; end; + pages := arenas * (ARENA_SIZE div 4096); + for i := 0 to pages - 1 do begin + slot := leaf(address + uint64(i) * 4096); + if slot = nil then begin unlockIRQ(saved); exit; end; + if (slot^ and (PTE_OWNED or 1)) <> (PTE_OWNED or 1) then begin unlockIRQ(saved); exit; end; + end; + releaseMapped(address, pages); + bitmap_clear_range(arenaBitmap, idx, arenas); + release_region := true; + unlockIRQ(saved); +end; +procedure init; +var storage: uint64; +begin + root := PUInt64(PtrUInt(pageRoot and ADDRESS_MASK)); + leafFlags := 3; + if enableNX then leafFlags := leafFlags or (uint64(1) shl 63); + tablePages := 0; failAfter := -1; + storage := arch.x86_64.memory.physical.alloc_page(true); + if storage = 0 then fail('arena metadata allocation'); + arenaBitmap := PUInt32(PtrUInt(storage)); + zero(arenaBitmap, 4096); +end; +procedure UnitTest; +const TEST_ADDRESS = HEAP_BASE + $800000; + TEST_PATTERN = uint64($FEDCBA9876543210); +var before, frame, removed, highFrame: uint64; region: PtrUInt; passed, failed: uint32; + procedure Assert(condition: boolean; name: pchar); + begin + if condition then begin inc(passed); puts('PASS: '); end + else begin inc(failed); puts('FAIL: '); end; + puts(name); puts(#13#10); + if not condition then bootstrap_halt; + end; +begin + passed := 0; failed := 0; before := free_count; + frame := arch.x86_64.memory.physical.alloc_page(true); + Assert(frame <> 0, 'VMM test frame allocation'); + Assert(map_page(TEST_ADDRESS, frame, false), 'VMM high virtual mapping'); + Assert(not map_page(TEST_ADDRESS, frame, false) and + not map_page(TEST_ADDRESS + 1, frame, false), 'VMM rejects duplicate/unaligned mapping'); + Assert(translate(TEST_ADDRESS + 123) = frame + 123, 'VMM translation includes page offset'); + PUInt64(TEST_ADDRESS)^ := TEST_PATTERN; + Assert(PUInt64(PtrUInt(frame))^ = TEST_PATTERN, 'VMM aliases the expected physical frame'); + Assert(unmap_page(TEST_ADDRESS, removed) and (removed = frame) and + (translate(TEST_ADDRESS) = 0), 'VMM unmap invalidates translation'); + Assert(arch.x86_64.memory.physical.free_page(frame), 'VMM returns backing frame'); + Assert((free_count = before) and (tablePages = 0), 'VMM prunes empty tables without leaks'); + failAfter := 2; + region := allocate_region(1); + failAfter := -1; + Assert((region = 0) and (free_count = before) and (tablePages = 0), 'VMM rolls back partial table allocation on OOM'); + failAfter := 12; + region := allocate_region(1); + failAfter := -1; + Assert((region = 0) and (free_count = before) and (tablePages = 0), 'VMM rolls back partially populated arenas on OOM'); + highFrame := alloc_high; + if highFrame <> 0 then begin + Assert(map_page(TEST_ADDRESS, highFrame, false), 'VMM maps physical RAM above 4 GiB'); + PUInt64(TEST_ADDRESS)^ := $123456789ABCDEF0; + Assert((PUInt64(TEST_ADDRESS)^ = $123456789ABCDEF0) and + (translate(TEST_ADDRESS) = highFrame), 'VMM reads and writes physical RAM above 4 GiB'); + Assert(unmap_page(TEST_ADDRESS, removed) and + arch.x86_64.memory.physical.free_page(highFrame), 'VMM releases high physical RAM'); + end else puts('SKIP: no available physical RAM above 4 GiB'#13#10); + region := allocate_region(1); + Assert(region <> 0, 'VMM contiguous arena backed by 4 KiB frames'); + PUInt64(region)^ := 1; + PUInt64(region + ARENA_SIZE - 8)^ := 2; + Assert((PUInt64(region)^ = 1) and (PUInt64(region + ARENA_SIZE - 8)^ = 2), 'VMM first/last arena pages writable'); + Assert(release_region(region, 1), 'VMM releases complete arena'); + Assert((free_count = before) and (tablePages = 0), 'VMM final frame accounting restored'); + field('VMM_TESTS_FAILED', failed); +end; +end. diff --git a/src/arch/x86_64/platform/arch.platform.cpu.pas b/src/arch/x86_64/platform/arch.platform.cpu.pas new file mode 100644 index 00000000..849be299 --- /dev/null +++ b/src/arch/x86_64/platform/arch.platform.cpu.pas @@ -0,0 +1,35 @@ +// Copyright 2026 Aaron Hance +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +{ Build-selected x86_64 adapter. } +unit arch.platform.cpu; +interface +{$i ../../common/platform.cpu.inc} +implementation +uses arch.x86_64.util, memory.raw; +function lock: PtrUInt; +begin lock := arch.x86_64.util.lockIRQ; end; +procedure unlock(flags: PtrUInt); +begin if (flags and $200) <> 0 then asm sti end; end; +procedure idle; +begin asm hlt end; end; +procedure outb(port: uint16; value: uint8); +begin arch.x86_64.util.outb(port, value); end; +function inb(port: uint16): uint8; +begin inb := arch.x86_64.util.inb(port); end; +procedure copyBytes(source, dest: PtrUInt; bytes: SizeUInt); +begin + memory.raw.move(pointer(source), pointer(dest), bytes); +end; +procedure zero(dest: pointer; bytes: SizeUInt); +begin arch.x86_64.util.zero(dest, bytes); end; +end. diff --git a/src/arch/x86_64/platform/arch.platform.diagnostics.pas b/src/arch/x86_64/platform/arch.platform.diagnostics.pas new file mode 100644 index 00000000..1b9176b3 --- /dev/null +++ b/src/arch/x86_64/platform/arch.platform.diagnostics.pas @@ -0,0 +1,32 @@ +// Copyright 2026 Aaron Hance +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +{ Build-selected x86_64 adapter. } +unit arch.platform.diagnostics; +interface +{$i ../../common/platform.diagnostics.inc} +implementation +uses debug.early; +procedure log(tag, message: pchar); +begin debug.early.puts(message); debug.early.puts(#13#10); end; +procedure push_trace(name: pchar); +begin end; +procedure pop_trace; +begin end; +procedure fatal(tag, message: pchar); +begin log(tag, message); bootstrap_halt; end; +procedure testSummary(tag, passedKey, failedKey: pchar; passed, failed: uint32); +begin + debug.early.field(passedKey, passed); + debug.early.field(failedKey, failed); +end; +end. diff --git a/src/arch/x86_64/platform/arch.platform.memory.pas b/src/arch/x86_64/platform/arch.platform.memory.pas new file mode 100644 index 00000000..253ed8b7 --- /dev/null +++ b/src/arch/x86_64/platform/arch.platform.memory.pas @@ -0,0 +1,38 @@ +// Copyright 2026 Aaron Hance +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +unit arch.platform.memory; +interface +uses arch.x86_64.memory.virtual; +const MAX_HEAP_REGIONS = arch.x86_64.memory.virtual.ARENA_COUNT; +{$i ../../common/platform.memory.inc} +implementation +uses arch.x86_64.memory.physical, arch.platform.diagnostics; +function allocateHeapPage: PtrUInt; +begin allocateHeapPage := allocate_region(1); end; +function allocateLarge(pages: uint32): PtrUInt; +begin allocateLarge := allocate_region(pages); end; +procedure release(address: PtrUInt; pages: uint32); +begin + if not release_region(address, pages) then fatal('LMM', 'Invalid heap region release'); +end; +function identityReserve(address: PtrUInt): pointer; +begin + identityReserve := nil; + if address < uint64($100000000) then begin + address := (address shr 22) shl 22; + if reserve_range(address, $400000) then identityReserve := pointer(address); + end; +end; +function freeBacking: uint64; +begin freeBacking := free_count; end; +end. diff --git a/src/arch/x86_64/platform/arch.platform.process.pas b/src/arch/x86_64/platform/arch.platform.process.pas new file mode 100644 index 00000000..48e2b2b1 --- /dev/null +++ b/src/arch/x86_64/platform/arch.platform.process.pas @@ -0,0 +1,23 @@ +// Copyright 2026 Aaron Hance +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +{ Build-selected x86_64 adapter. } +unit arch.platform.process; +interface +{$i ../../common/platform.process.inc} +implementation +uses arch.x86_64.context, arch.x86_64.irq; +function prepare(stack: pointer; bytes: SizeUInt; entry: pointer): PtrUInt; +begin prepare := arch.x86_64.context.prepare(stack, bytes, entry, 0); end; +function timerTicks: uint32; +begin timerTicks := uint32(arch.x86_64.irq.ticks); end; +end. diff --git a/src/arch/x86_64/proc/arch.x86_64.proc.sched.pas b/src/arch/x86_64/proc/arch.x86_64.proc.sched.pas new file mode 100644 index 00000000..87bdb690 --- /dev/null +++ b/src/arch/x86_64/proc/arch.x86_64.proc.sched.pas @@ -0,0 +1,173 @@ +// Copyright 2026 Aaron Hance +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +{ Adapter between saved CPU contexts and the shared process manager. } +unit arch.x86_64.proc.sched; +interface +procedure init; +procedure UnitTest; +implementation +uses proc.mgr, proc.types, io.stdio, core.strings, driver.storage.fdtable, arch.x86_64.context, arch.x86_64.util, + arch.x86_64.irq, arch.common.legacyirq, memory.heap, memory.raw, + core.ds.dynamic, debug.early; +var + switches, cleaned, returned, woke, exited, forbidden: uint32; + childID, parentID: uint32; + cleanupCtx: PProcessContext; + commandParams: PParamList; + commandBuffer: POutBuf; + commandValid, sleepValid, asyncRetained: boolean; + asyncEntry: PFileDescriptor; + fdReleased: uint32; +function selectProcess(saved: PtrUInt): PtrUInt; +begin + CurrentProcess^.SavedStack := saved; + CurrentProcess := scheduler_pick_next; + inc(switches); + selectProcess := CurrentProcess^.SavedStack; +end; +procedure init; +begin + { Caller owns IRQ exclusion; proc.mgr.init must have installed idle. } + setSelector(@selectProcess); +end; +procedure cleanupResource(handle: void); +begin + inc(cleaned); + { Re-enter cleanup to verify the resource was detached before invocation. } + unbindResource(cleanupCtx, handle); + kfree(handle); +end; +procedure parentWorker(ctx: PProcessContext); +var flags: PtrUInt; +begin + flags := lockIRQ; + if ctx^.PendingMsg <> smChildExited then ctx^.State := psAwaiting; + unlockIRQ(flags); + proc_await; + if (ctx^.PendingMsg = smChildExited) and (PtrUInt(ctx^.MsgData) = childID) then inc(woke); + inc(returned); +end; +procedure childWorker(ctx: PProcessContext); +begin + inc(exited); + proc_exit(7); + inc(forbidden); +end; +procedure forbiddenWorker(ctx: PProcessContext); +begin inc(forbidden); end; +procedure closeChildFD(data: pointer); +begin inc(fdReleased); kfree(void(data)); end; +procedure commandWorker(params: PParamList; stdin_buf, stdout_buf, stderr_buf: POutBuf); +type PState = ^TProcessState; +var start: uint64; held: PProcessContext; +begin + commandValid := (params = commandParams) and (stdin_buf = commandBuffer) and + (stdout_buf = commandBuffer) and (stderr_buf = commandBuffer); + start := ticks; + proc_sleep_ms(2); + sleepValid := ticks - start >= (uint64(2) * PIT_INPUT_HZ + PIT_DIVISOR * 1000 - 1) div (PIT_DIVISOR * 1000); + held := findByID(childID); + if held <> nil then begin + while (PState(@held^.State)^ <> psFinished) and (ticks - start < 1000) do proc_yield; + asyncRetained := (held^.State = psFinished) and fd_has_async(asyncEntry); + fd_end_async(asyncEntry); + end; +end; +procedure UnitTest; +type PCommandBuffer = ^TCommandBuffer; +var + input: PCommandBuffer; + parent, child, killed: PProcessContext; + beforeFree: SizeUInt; + started: uint64; + flags: PtrUInt; + list: PDList; + slot: ^PtrUInt; + i, failed, passed, baseCount: uint32; + procedure Assert(condition: boolean; name: pchar); + begin + if condition then begin inc(passed); puts('PASS: '); end + else begin inc(failed); puts('FAIL: '); end; + puts(name); puts(#13#10); + end; +begin + failed := 0; passed := 0; + beforeFree := lmm_total_free; + baseCount := processCount; + list := DL_New(sizeof(PtrUInt)); + for i := 0 to 79 do begin + slot := pointer(DL_Add(list)); slot^ := PtrUInt($1234567800000000) + i; + end; + slot := pointer(DL_Insert(list, 1)); slot^ := 777; + Assert(PUInt64(DL_Get(list, 2))^ = uint64($1234567800000001), 'native dynamic list overlapping insertion'); + DL_Delete(list, 1); + Assert(PUInt64(DL_Get(list, 79))^ = uint64($123456780000004F), 'native dynamic list growth and deletion'); + DL_Clear(list); DL_ShrinkToFit(list); + slot := pointer(DL_Add(list)); + Assert((slot <> nil) and (slot^ = 0), 'empty dynamic list regrows with zeroed slots'); + DL_Free(list); + Assert(lmm_total_free = beforeFree, 'dynamic list releases allocation ownership'); + + cleaned := 0; returned := 0; woke := 0; exited := 0; forbidden := 0; + parent := createSuspended('lifecycle parent', @parentWorker, nil, 1); + child := createSuspended('lifecycle child', @childWorker, nil, 3); + killed := createSuspended('never run', @forbiddenWorker, nil, 1); + parentID := parent^.ProcessID; childID := child^.ProcessID; + child^.ParentID := parentID; + asyncRetained := false; fdReleased := 0; + asyncEntry := @PFDTable(child^.FDTable)^.Entries[0]; + asyncEntry^.InUse := true; + asyncEntry^.FSPrivate := pointer(kalloc(11)); + asyncEntry^.FSCloseHook := @closeChildFD; + fd_begin_async(asyncEntry); + cleanupCtx := parent; + bindResource(parent, rkCustom, kalloc(37), @cleanupResource); + Assert((findByID(parentID) = parent) and (findByName('lifecycle child') = child), 'process lookup preserves native pointers'); + Assert((parent^.State = psSuspended) and (child^.State = psSuspended), 'processes stay suspended until published'); + kill(killed^.ProcessID); + Assert(killed^.State = psFinished, 'killed suspended process never published for execution'); + input := PCommandBuffer(kalloc(sizeof(TCommandBuffer))); + memory.raw.move(pchar('echo scheduled'), input, 14); + commandParams := getParams(input^); + kfree(void(input)); + commandBuffer := createOutBuf(1); + commandValid := false; sleepValid := false; + runCommand('scheduled ECHO', findCommand('echo')^.method, commandParams, + nil, commandBuffer, nil); + runCommand('lifecycle command', @commandWorker, commandParams, + commandBuffer, commandBuffer, commandBuffer); + parent^.State := psReady; child^.State := psReady; + started := ticks; + masks($FE, $FF); + asm sti end; + repeat + asm pause end; + until (processCount = baseCount) or (ticks - started > 16000); + flags := lockIRQ; + masks($FF, $FF); + Assert((flags and $200) <> 0, 'process lifecycle preserves interrupt state'); + Assert((returned = 1) and (exited = 1) and (forbidden = 0), 'return and explicit exit never resume finished processes'); + Assert(woke = 1, 'child completion wakes awaiting parent'); + Assert(cleaned = 1, 'resource callback runs exactly once under reentry'); + Assert((processCount = baseCount) and (CurrentProcess = IdleProcess), 'reaper removes finished processes and retains system tasks'); + Assert(commandValid, 'command arguments and streams installed before execution'); + Assert(sleepValid, 'process sleep uses the programmed PIT rate'); + Assert(asyncRetained and (fdReleased = 1), 'reaper waits for async FD completion then closes exactly once'); + Assert(stringEquals(commandBuffer^.buf, 'scheduled '#10), 'shared ECHO executes through scheduler'); + freeParams(commandParams); freeOutBuf(commandBuffer); + Assert(lmm_total_free = beforeFree, 'process contexts, stacks, CWD and resources freed'); + field('PROCESS_TESTS_PASSED', passed); + field('PROCESS_TESTS_FAILED', failed); + if failed <> 0 then bootstrap_halt; +end; +end. diff --git a/src/arch/x86_64/system.pas b/src/arch/x86_64/system.pas new file mode 100644 index 00000000..8b7538eb --- /dev/null +++ b/src/arch/x86_64/system.pas @@ -0,0 +1,50 @@ +// Copyright 2026 Aaron Hance +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +{ Minimal bootstrap runtime. Unsupported runtime operations deliberately have + no implementation, so accidental dependencies fail at compile/link time. } +unit system; + +interface + +type SizeInt = int64; + SizeUInt = qword; + +{$i ../common/system.types.inc} + +type + { SysV AMD64 nonvolatile register layout. No exception-unwinding helpers + are provided at this bootstrap stage. FPC still requires these types. } + jmp_buf = packed record + rbx, rbp, r12, r13, r14, r15, rsp, rip: uint64; + end; + PJmp_buf = ^jmp_buf; + PExceptAddr = ^TExceptAddr; + TExceptAddr = record + buf: PJmp_buf; + next: PExceptAddr; + frametype: longint; + end; + +procedure bootstrap_halt; external name 'bootstrap_halt'; +function Trunc(d: Double): int64; + +implementation + +{ SysV AMD64 passes Double in XMM0. CVTTSD2SI rounds toward zero without + changing the caller's x87 control word or MXCSR rounding mode. } +function Trunc(d: Double): int64; assembler; nostackframe; +asm + cvttsd2si rax, xmm0 +end; + +end. diff --git a/src/asuro.pas b/src/asuro.pas index 8f26b7e3..dd7f0b57 100644 --- a/src/asuro.pas +++ b/src/asuro.pas @@ -27,7 +27,7 @@ uses io.syslog, core.panic, arch.x86.multiboot, - arch.x86.proc.sched; + arch.x86.proc.sched, arch.x86.startup; procedure kmain(mbinfo: Pmultiboot_info_t; mbmagic: uint32); stdcall; diff --git a/src/boot/boot.multiboot.pas b/src/boot/boot.multiboot.pas new file mode 100644 index 00000000..f9aa36e5 --- /dev/null +++ b/src/boot/boot.multiboot.pas @@ -0,0 +1,75 @@ +// Copyright 2021 Kieron Morris & Aaron Hance +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +{ Multiboot v1 wire layout: fixed-width fields on every architecture. } +unit boot.multiboot; +interface +const MULTIBOOT_BOOTLOADER_MAGIC = $2BADB002; +type + Pelf_section_header_table_t = ^elf_section_header_table_t; + elf_section_header_table_t = packed record + num: uint32; + size: uint32; + addr: uint32; + shndx: uint32; + end; + + Pmultiboot_info_t = ^multiboot_info_t; + multiboot_info_t = packed record + flags: uint32; + mem_lower: uint32; + mem_upper: uint32; + boot_device: uint32; + cmdline: uint32; + mods_count: uint32; + mods_addr: uint32; + elf_sec: elf_section_header_table_t; + mmap_length: uint32; + mmap_addr: uint32; + drives_legnth : uint32; + drives_addr: uint32; + config_table : uint32; + boot_loader_name : uint32; + apm_table: uint32; + vbe_control_info : uint32; + vbe_mode_info : uint32; + vbe_mode : uint16; + vbe_interface_seg : uint16; + vbe_interface_off : uint16; + vbe_interface_len : uint16; + framebuffer_addr : uint64; + framebuffer_pitch : uint32; + framebuffer_width : uint32; + framebuffer_height: uint32; + framebuffer_bpp : uint8; + end; + + Pmodule_t = ^module_t; + module_t = packed record + mod_start: uint32; + mod_end: uint32; + name: uint32; + reserved: uint32; + end; + + Pmemory_map_t = ^memory_map_t; + memory_map_t = packed record + size: uint32; + base_addr : uint64; + length : uint64; + mtype: uint32; + end; + +implementation +end. diff --git a/src/core/core.util.pas b/src/core/core.util.pas index 2b1b4e73..f6c233fd 100644 --- a/src/core/core.util.pas +++ b/src/core/core.util.pas @@ -1,4 +1,5 @@ // Copyright 2021 Kieron Morris +// Copyright 2026 Aaron Hance // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -33,8 +34,8 @@ function switchendian32(b : uint32) : uint32; function getWord(i : uint32; hi : boolean) : uint16; function getByte(i : uint32; index : uint8) : uint8; -procedure memset(location : uint32; value : uint8; size : uint32); -procedure memcpy(source : uint32; dest : uint32; size : uint32); +procedure memset(location : PtrUInt; value : uint8; size : SizeUInt); +procedure memcpy(source : PtrUInt; dest : PtrUInt; size : SizeUInt); function BCDToUint8(bcd : uint8) : uint8; function HexCharToDecimal(hex : char) : uint8; @@ -42,26 +43,7 @@ function HexCharToDecimal(hex : char) : uint8; function abs(x : sint32) : uint32; implementation -{$ifdef CPU386} -uses - arch.x86.util; - -const - MEMCPY_X86_SMALL_COPY_THRESHOLD = 16; -{$endif} - -procedure scalar_memcpy(source : uint32; dest : uint32; size : uint32); -var - src, dst : puint8; - i : uint32; -begin - if size = 0 then exit; - for i := 0 to size - 1 do begin - src := puint8(source + i); - dst := puint8(dest + i); - dst^ := src^; - end; -end; +uses memory.raw, arch.platform.cpu; function abs(x : sint32) : uint32; var @@ -129,30 +111,15 @@ begin switchendian:= (lo(b) SHL 4) OR hi(b); end; -procedure memset(location : uint32; value : uint8; size : uint32); -var - loc : puint8; - i : uint32; - +procedure memset(location : PtrUInt; value : uint8; size : SizeUInt); begin - if size = 0 then exit; - for i:=0 to size-1 do begin - loc:= puint8(location + i); - loc^:= value; - end; + memory.raw.fill(pointer(location), value, size); end; -procedure memcpy(source : uint32; dest : uint32; size : uint32); +procedure memcpy(source : PtrUInt; dest : PtrUInt; size : SizeUInt); begin if size = 0 then exit; -{$ifdef CPU386} - if size < MEMCPY_X86_SMALL_COPY_THRESHOLD then - scalar_memcpy(source, dest, size) - else - arch.x86.util.__REP_MOVSB_memcpy(source, dest, size); -{$else} - scalar_memcpy(source, dest, size); -{$endif} + arch.platform.cpu.copyBytes(source, dest, size); end; function getWord(i : uint32; hi : boolean) : uint16; diff --git a/src/core/ds/core.ds.dynamic.pas b/src/core/ds/core.ds.dynamic.pas new file mode 100644 index 00000000..adee400e --- /dev/null +++ b/src/core/ds/core.ds.dynamic.pas @@ -0,0 +1,183 @@ +// Copyright 2026 Aaron Hance +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +{ Dynamic-list implementation shared with the legacy core.ds.lists API. + Original interface: Kieron Morris. Capacity remains bounded to uint32 bytes; + addresses are native-width. Operations require caller-owned synchronization. } +unit core.ds.dynamic; +interface +type + PDList = ^TDList; + TDList = record + Count: uint32; + Data: void; + ElementSize, DataSize: uint32; + end; +function DL_New(ElementSize : uint32) : PDList; +function DL_Add(DList : PDList) : Void; +function DL_Delete(DList : PDList; idx : uint32) : boolean; +function DL_Get(DList : PDList; idx : uint32) : Void; +function DL_Set(DList : PDList; idx : uint32; elm : puint32) : boolean; +function DL_Size(DList : PDList) : uint32; +procedure DL_Free(DList : PDList); +function DL_IndexOf(DList : PDList; elm : puint32) : uint32; +function DL_Contains(DList : PDList; elm : puint32) : boolean; +function DL_Concat(DList1 : PDList; DList2 : PDList) : PDList; +procedure DL_Clear(DList : PDList); +function DL_Insert(DList : PDList; idx : uint32) : Void; +function DL_Capacity(DList : PDList) : uint32; +procedure DL_Reserve(DList : PDList; NewCapacity : uint32); +procedure DL_ShrinkToFit(DList : PDList); +implementation +uses memory.heap, memory.raw; +function resize(list: PDList; bytes: uint32): boolean; +var data: void; +begin + resize := false; + if bytes < list^.Count * list^.ElementSize then exit; + if bytes = list^.DataSize then begin resize := true; exit; end; + data := nil; + if bytes <> 0 then begin + data := kalloc(bytes); + if data = nil then exit; + memory.raw.move(pointer(list^.Data), pointer(data), list^.Count * list^.ElementSize); + end; + kfree(list^.Data); + list^.Data := data; list^.DataSize := bytes; + resize := true; +end; +function ensure(list: PDList; count: uint32): boolean; +var bytes, capacity: uint32; +begin + ensure := false; + if (list = nil) or (list^.ElementSize = 0) then exit; + if count > High(uint32) div list^.ElementSize then exit; + bytes := count * list^.ElementSize; + if bytes <= list^.DataSize then begin ensure := true; exit; end; + capacity := list^.DataSize; + if capacity <= High(uint32) div 2 then capacity := capacity * 2; + if capacity < bytes then capacity := bytes; + ensure := resize(list, capacity); +end; +function DL_New(ElementSize: uint32): PDList; +var list: PDList; count: uint32; +begin + DL_New := nil; + if (ElementSize = 0) or (ElementSize > High(uint32) div 4) then exit; + list := PDList(kalloc(sizeof(TDList))); + if list = nil then exit; + list^.ElementSize := ElementSize; + if ElementSize > 128 then count := 4 else + if ElementSize > 64 then count := 8 else + if ElementSize > 32 then count := 16 else + if ElementSize > 16 then count := 32 else count := 64; + if not ensure(list, count) then begin kfree(void(list)); exit; end; + DL_New := list; +end; +function DL_Insert(DList: PDList; idx: uint32): void; +var address: PtrUInt; +begin + DL_Insert := nil; + if DList = nil then exit; + if (idx > DList^.Count) or (DList^.Count = High(uint32)) then exit; + if not ensure(DList, DList^.Count + 1) then exit; + address := PtrUInt(DList^.Data) + SizeUInt(idx) * DList^.ElementSize; + memory.raw.move(pointer(address), pointer(address + DList^.ElementSize), + (DList^.Count - idx) * DList^.ElementSize); + memory.raw.fill(pointer(address), 0, DList^.ElementSize); + inc(DList^.Count); + DL_Insert := void(address); +end; +function DL_Add(DList: PDList): void; +begin + DL_Add := nil; + if DList <> nil then DL_Add := DL_Insert(DList, DList^.Count); +end; +function DL_Delete(DList: PDList; idx: uint32): boolean; +var address: PtrUInt; +begin + DL_Delete := false; + if DList = nil then exit; + if idx >= DList^.Count then exit; + address := PtrUInt(DList^.Data) + SizeUInt(idx) * DList^.ElementSize; + memory.raw.move(pointer(address + DList^.ElementSize), pointer(address), + (DList^.Count - idx - 1) * DList^.ElementSize); + dec(DList^.Count); + { Retain capacity; deletion in the scheduler must not allocate or thrash. + Explicit DL_ShrinkToFit remains available at a suitable safe point. } + DL_Delete := true; +end; +function DL_Get(DList: PDList; idx: uint32): void; +begin + DL_Get := nil; + if DList = nil then exit; + if idx < DList^.Count then + DL_Get := void(PtrUInt(DList^.Data) + SizeUInt(idx) * DList^.ElementSize); +end; +function DL_Set(DList: PDList; idx: uint32; elm: PUInt32): boolean; +var target: void; +begin + target := DL_Get(DList, idx); + DL_Set := (target <> nil) and (elm <> nil); + if DL_Set then memory.raw.move(pointer(elm), pointer(target), DList^.ElementSize); +end; +function DL_Size(DList: PDList): uint32; +begin + DL_Size := 0; + if DList <> nil then DL_Size := DList^.Count; +end; +procedure DL_Free(DList: PDList); +begin + if DList = nil then exit; + kfree(DList^.Data); kfree(void(DList)); +end; +function DL_IndexOf(DList: PDList; elm: PUInt32): uint32; +var offset: PtrUInt; +begin + DL_IndexOf := High(uint32); + if (DList = nil) or (elm = nil) then exit; + if PtrUInt(elm) < PtrUInt(DList^.Data) then exit; + offset := PtrUInt(elm) - PtrUInt(DList^.Data); + if (offset < SizeUInt(DList^.Count) * DList^.ElementSize) and + (offset mod DList^.ElementSize = 0) then DL_IndexOf := offset div DList^.ElementSize; +end; +function DL_Contains(DList: PDList; elm: PUInt32): boolean; +begin DL_Contains := DL_IndexOf(DList, elm) <> High(uint32); end; +function DL_Concat(DList1, DList2: PDList): PDList; +var count, oldCount: uint32; +begin + DL_Concat := nil; + if (DList1 = nil) or (DList2 = nil) then exit; + if DList1^.ElementSize <> DList2^.ElementSize then exit; + count := DList2^.Count; oldCount := DList1^.Count; + if count > High(uint32) - oldCount then exit; + if not ensure(DList1, oldCount + count) then exit; + memory.raw.move(pointer(DList2^.Data), + pointer(PtrUInt(DList1^.Data) + SizeUInt(oldCount) * DList1^.ElementSize), + count * DList1^.ElementSize); + DList1^.Count := oldCount + count; + DL_Concat := DList1; +end; +procedure DL_Clear(DList: PDList); +begin if DList <> nil then DList^.Count := 0; end; +function DL_Capacity(DList: PDList): uint32; +begin + DL_Capacity := 0; + if DList <> nil then DL_Capacity := DList^.DataSize div DList^.ElementSize; +end; +procedure DL_Reserve(DList: PDList; NewCapacity: uint32); +begin ensure(DList, NewCapacity); end; +procedure DL_ShrinkToFit(DList: PDList); +begin + if DList <> nil then resize(DList, DList^.Count * DList^.ElementSize); +end; +end. diff --git a/src/core/ds/core.ds.lists.pas b/src/core/ds/core.ds.lists.pas index 7f78e468..09518497 100644 --- a/src/core/ds/core.ds.lists.pas +++ b/src/core/ds/core.ds.lists.pas @@ -2,6 +2,7 @@ // core.ds.lists.pas // // Copyright 2021 Kieron Morris +// Copyright 2026 Aaron Hance // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -22,6 +23,7 @@ unit core.ds.lists; interface uses + core.ds.types, core.ds.dynamic, io.syslog, memory.heap, debug.tracer, @@ -31,56 +33,17 @@ type {** @abstract Pointer to a managed linked list node. **} - PLinkedList = ^TLinkedList; - - {** - @abstract Managed linked list node record. - @field Previous Pointer to the previous node. - @field Data Pointer to the stored data. - @field Next Pointer to the next node. - **} - TLinkedList = record - Previous : PLinkedList; - Data : void; - Next : PLinkedList; - end; - - {** - @abstract Pointer to the base of a managed linked list. - **} - PLinkedListBase = ^TLinkedListBase; - - {** - @abstract Base record for a managed linked list. - @field Count Number of elements in the list. - @field Head Pointer to the first node. - @field ElementSize Size (in bytes) of each element. - **} - TLinkedListBase = record - Count : uint32; - Head : PLinkedList; - ElementSize : uint32; - end; + PLinkedList = core.ds.types.PLinkedList; + TLinkedList = core.ds.types.TLinkedList; + PLinkedListBase = core.ds.types.PLinkedListBase; + TLinkedListBase = core.ds.types.TLinkedListBase; {** @abstract Pointer to a dynamic list. **} - PDList = ^TDList; + PDList = core.ds.dynamic.PDList; + TDList = core.ds.dynamic.TDList; - {** - @abstract Record for a dynamic list. - @field Count Number of elements in use. - @field Data Pointer to the underlying buffer. - @field ElementSize Size (in bytes) of each element. - @field DataSize Total allocated bytes for Data. - **} - TDList = record - Count : uint32; // number of elements in use - Data : void; // pointer to the underlying buffer - ElementSize : uint32; // size of each element in bytes - DataSize : uint32; // total allocated bytes for Data - end; - { Function/Procedure definitions } // ******************* String Linked List Routines ******************* @@ -727,384 +690,78 @@ end; @returns Pointer to the newly allocated dynamic list. **} function DL_New(ElementSize : uint32) : PDList; -var - DL : PDList; - initialBytes : uint32; begin - DL := PDList(kalloc(sizeof(TDList))); - DL^.ElementSize := ElementSize; - DL^.Count := 0; - - // Pick an initial capacity based on the element size - if ElementSize > 128 then - initialBytes := ElementSize * 4 - else if ElementSize > 64 then - initialBytes := ElementSize * 8 - else if ElementSize > 32 then - initialBytes := ElementSize * 16 - else if ElementSize > 16 then - initialBytes := ElementSize * 32 - else - initialBytes := ElementSize * 64; - - DL^.Data := kalloc(initialBytes); - DL^.DataSize := initialBytes; - - DL_New := DL; + DL_New := core.ds.dynamic.DL_New(ElementSize); end; -{** - @abstract Adds a new element to the dynamic list. - @param DList Pointer to the dynamic list. - @returns Pointer to the newly allocated slot. - @discussion Resizes the underlying buffer (doubling its size) if necessary. -**} function DL_Add(DList : PDList) : Void; -var - newElem : pointer; - oldData : pointer; - oldSize : uint32; begin - // Check if we need to resize - if ((DList^.Count + 1) * DList^.ElementSize) > DList^.DataSize then - begin - push_trace('core.ds.lists.DL_Add: Resizing'); - oldData := DList^.Data; - oldSize := DList^.DataSize; - - DList^.DataSize := DList^.DataSize * 2; - DList^.Data := kalloc(DList^.DataSize); - memset(uint32(DList^.Data), 0, DList^.DataSize); - - // Copy old data into the new buffer - memcpy(uint32(oldData), uint32(DList^.Data), oldSize); - - kfree(oldData); - end; - - // Address of the new element - newElem := pointer(uint32(DList^.Data) + (DList^.Count * DList^.ElementSize)); - // Zero out the newly added slot - memset(uint32(newElem), 0, DList^.ElementSize); - - DList^.Count := DList^.Count + 1; - DL_Add := newElem; + DL_Add := core.ds.dynamic.DL_Add(DList); end; -{** - @abstract Deletes the element at the specified index from the dynamic list. - @param DList Pointer to the dynamic list. - @param idx Zero-based index of the element to delete. - @returns True if deletion was successful, false otherwise. - @discussion Shifts subsequent elements left and shrinks the buffer if underutilized. -**} function DL_Delete(DList : PDList; idx : uint32) : boolean; -var - sourcePtr, destPtr : pointer; - shiftSize : uint32; - oldData : pointer; - oldSize : uint32; begin - if (idx >= DList^.Count) then - begin - DL_Delete := false; - exit; - end; - - // If not removing the last element, shift the tail left by one element. - if idx < (DList^.Count - 1) then - begin - sourcePtr := pointer(uint32(DList^.Data) + ((idx + 1) * DList^.ElementSize)); - destPtr := pointer(uint32(DList^.Data) + (idx * DList^.ElementSize)); - shiftSize := (DList^.Count - idx - 1) * DList^.ElementSize; - memcpy(uint32(sourcePtr), uint32(destPtr), shiftSize); - end; - - // Decrement element count - DList^.Count := DList^.Count - 1; - - // Possibly shrink the buffer if usage is very low - if (DList^.Count > 0) and ((DList^.Count * DList^.ElementSize) < (DList^.DataSize div 2)) then - begin - oldData := DList^.Data; - oldSize := DList^.DataSize; - - DList^.DataSize := DList^.DataSize div 2; - if DList^.DataSize < (DList^.Count * DList^.ElementSize) then - DList^.DataSize := (DList^.Count * DList^.ElementSize); - - DList^.Data := kalloc(DList^.DataSize); - memset(uint32(DList^.Data), 0, DList^.DataSize); - - memcpy(uint32(oldData), uint32(DList^.Data), (DList^.Count * DList^.ElementSize)); - kfree(oldData); - end; - - DL_Delete := true; + DL_Delete := core.ds.dynamic.DL_Delete(DList, idx); end; -{** - @abstract Retrieves the element data at the specified index in the dynamic list. - @param DList Pointer to the dynamic list. - @param idx Zero-based index of the desired element. - @returns Pointer to the element data, or nil if index is out of range. -**} function DL_Get(DList : PDList; idx : uint32) : Void; begin - if (idx >= DList^.Count) then - begin - DL_Get := nil; - exit; - end; - - DL_Get := pointer(uint32(DList^.Data) + (idx * DList^.ElementSize)); + DL_Get := core.ds.dynamic.DL_Get(DList, idx); end; -{** - @abstract Sets the element at the specified index by copying data from the provided pointer. - @param DList Pointer to the dynamic list. - @param idx Zero-based index of the element to set. - @param elm Pointer to the source data. - @returns True if the operation was successful, false otherwise. -**} function DL_Set(DList : PDList; idx : uint32; elm : puint32) : boolean; -var - targetPtr : pointer; begin - if (idx >= DList^.Count) then - begin - DL_Set := false; - exit; - end; - - // Copy the data from elm into the dynamic list - targetPtr := pointer(uint32(DList^.Data) + (idx * DList^.ElementSize)); - memcpy(uint32(elm), uint32(targetPtr), DList^.ElementSize); - - DL_Set := true; + DL_Set := core.ds.dynamic.DL_Set(DList, idx, elm); end; -{** - @abstract Returns the number of elements currently in the dynamic list. - @param DList Pointer to the dynamic list. - @returns The element count. -**} function DL_Size(DList : PDList) : uint32; begin - DL_Size := DList^.Count; + DL_Size := core.ds.dynamic.DL_Size(DList); end; -{** - @abstract Frees the entire dynamic list and its underlying buffer. - @param DList Pointer to the dynamic list. -**} procedure DL_Free(DList : PDList); begin - if DList = nil then exit; - if DList^.Data <> nil then - kfree(DList^.Data); - kfree(void(DList)); + core.ds.dynamic.DL_Free(DList); end; -{** - @abstract Searches for an element in the dynamic list. - @param DList Pointer to the dynamic list. - @param elm Pointer to the element to find. - @returns The zero-based index of the element if found; otherwise, returns -1. - @discussion The search is based on pointer equality. -**} function DL_IndexOf(DList : PDList; elm : puint32) : uint32; -var - i : uint32; - temp : puint32; begin - for i := 0 to DList^.Count - 1 do - begin - temp := puint32(uint32(DList^.Data) + (i * DList^.ElementSize)); - if (temp = elm) then - begin - DL_IndexOf := i; - exit; - end; - end; - DL_IndexOf := uint32(-1); + DL_IndexOf := core.ds.dynamic.DL_IndexOf(DList, elm); end; -{** - @abstract Determines whether the dynamic list contains the specified element. - @param DList Pointer to the dynamic list. - @param elm Pointer to the element to search for. - @returns True if the element is present, false otherwise. -**} function DL_Contains(DList : PDList; elm : puint32) : boolean; begin - DL_Contains := (DL_IndexOf(DList, elm) <> uint32(-1)); + DL_Contains := core.ds.dynamic.DL_Contains(DList, elm); end; -{** - @abstract Concatenates two dynamic lists. - @param DList1 Pointer to the destination dynamic list. - @param DList2 Pointer to the source dynamic list. - @returns Pointer to the concatenated dynamic list (DList1), or nil if element sizes differ. -**} function DL_Concat(DList1 : PDList; DList2 : PDList) : PDList; -var - i : uint32; - ptr : pointer; begin - if DList1^.ElementSize <> DList2^.ElementSize then - begin - DL_Concat := nil; - exit; - end; - - for i := 0 to DList2^.Count - 1 do - begin - ptr := DL_Add(DList1); - memcpy(uint32(DL_Get(DList2, i)), uint32(ptr), DList1^.ElementSize); - end; - - DL_Concat := DList1; + DL_Concat := core.ds.dynamic.DL_Concat(DList1, DList2); end; -{** - @abstract Clears the dynamic list by resetting the count to zero. - @param DList Pointer to the dynamic list. - @discussion The underlying buffer is retained for fast reuse. -**} procedure DL_Clear(DList : PDList); begin - if (DList = nil) then exit; - DList^.Count := 0; - // To clear memory, uncomment the following line: - // memset(uint32(DList^.Data), 0, DList^.DataSize); + core.ds.dynamic.DL_Clear(DList); end; -{** - @abstract Inserts a new element at the specified index in the dynamic list. - @param DList Pointer to the dynamic list. - @param idx Zero-based index where the element should be inserted. - @returns Pointer to the newly allocated slot, or nil if idx is out of range. - @discussion Subsequent elements are shifted right. The buffer is resized if needed. -**} function DL_Insert(DList : PDList; idx : uint32) : Void; -var - newElem : pointer; - oldData : pointer; - oldSize : uint32; - shiftSrc : pointer; - shiftDest : pointer; - shiftSize : uint32; begin - DL_Insert := nil; - if (DList = nil) then exit; - if (idx > DList^.Count) then - exit; // cannot insert beyond the current count - - // Ensure there's space for one more element - if ((DList^.Count + 1) * DList^.ElementSize) > DList^.DataSize then - begin - oldData := DList^.Data; - oldSize := DList^.DataSize; - - DList^.DataSize := DList^.DataSize * 2; - DList^.Data := kalloc(DList^.DataSize); - memset(uint32(DList^.Data), 0, DList^.DataSize); - - memcpy(uint32(oldData), uint32(DList^.Data), oldSize); - kfree(oldData); - end; - - // Shift elements to the right if inserting in the middle - if (idx < DList^.Count) then - begin - shiftSrc := pointer(uint32(DList^.Data) + (idx * DList^.ElementSize)); - shiftDest := pointer(uint32(DList^.Data) + ((idx + 1) * DList^.ElementSize)); - shiftSize := (DList^.Count - idx) * DList^.ElementSize; - // Shift overlapping regions; if memcpy doesn't handle overlap, use memmove. - memcpy(uint32(shiftSrc), uint32(shiftDest), shiftSize); - end; - - // Allocate the new element slot - newElem := pointer(uint32(DList^.Data) + (idx * DList^.ElementSize)); - memset(uint32(newElem), 0, DList^.ElementSize); - - DList^.Count := DList^.Count + 1; - DL_Insert := newElem; + DL_Insert := core.ds.dynamic.DL_Insert(DList, idx); end; -{** - @abstract Returns the current capacity (number of elements) that the dynamic list can hold. - @param DList Pointer to the dynamic list. - @returns The capacity based on the allocated buffer size. -**} function DL_Capacity(DList : PDList) : uint32; begin - if DList = nil then - DL_Capacity := 0 - else - DL_Capacity := (DList^.DataSize div DList^.ElementSize); + DL_Capacity := core.ds.dynamic.DL_Capacity(DList); end; -{** - @abstract Ensures the dynamic list has enough space for at least NewCapacity elements. - @param DList Pointer to the dynamic list. - @param NewCapacity Desired capacity (in number of elements). - @discussion Reallocates the buffer if necessary. -**} procedure DL_Reserve(DList : PDList; NewCapacity : uint32); -var - neededBytes : uint32; - oldData : pointer; - oldSize : uint32; begin - if (DList = nil) then exit; - - neededBytes := NewCapacity * DList^.ElementSize; - if (neededBytes <= DList^.DataSize) then - exit; // Already sufficient - - oldData := DList^.Data; - oldSize := DList^.DataSize; - - DList^.DataSize := neededBytes; - DList^.Data := kalloc(DList^.DataSize); - - memset(uint32(DList^.Data), 0, DList^.DataSize); - memcpy(uint32(oldData), uint32(DList^.Data), oldSize); - - kfree(oldData); + core.ds.dynamic.DL_Reserve(DList, NewCapacity); end; -{** - @abstract Shrinks the dynamic list's allocated memory to fit its current count. - @param DList Pointer to the dynamic list. - @discussion If the list is empty, the buffer is freed. -**} procedure DL_ShrinkToFit(DList : PDList); -var - exactBytes : uint32; - oldData : pointer; begin - if (DList = nil) then exit; - exactBytes := DList^.Count * DList^.ElementSize; - if (exactBytes = 0) then - begin - // For an empty list, free the buffer and reset values. - kfree(DList^.Data); - DList^.Data := nil; - DList^.DataSize := 0; - exit; - end; - - if exactBytes >= DList^.DataSize then exit; // Already optimal - - oldData := DList^.Data; - DList^.Data := kalloc(exactBytes); - - memcpy(uint32(oldData), uint32(DList^.Data), exactBytes); - kfree(oldData); - DList^.DataSize := exactBytes; + core.ds.dynamic.DL_ShrinkToFit(DList); end; procedure TestAllLists; diff --git a/src/core/ds/core.ds.types.pas b/src/core/ds/core.ds.types.pas index c4aacce3..23f9ae16 100644 --- a/src/core/ds/core.ds.types.pas +++ b/src/core/ds/core.ds.types.pas @@ -1,4 +1,5 @@ // Copyright 2024 Aaron Hance +// Copyright 2026 Aaron Hance // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -22,6 +23,38 @@ unit core.ds.types; interface type + PLinkedList = ^TLinkedList; + + {** + @abstract Managed linked list node record. + @field Previous Pointer to the previous node. + @field Data Pointer to the stored data. + @field Next Pointer to the next node. + **} + TLinkedList = record + Previous : PLinkedList; + Data : void; + Next : PLinkedList; + end; + + {** + @abstract Pointer to the base of a managed linked list. + **} + PLinkedListBase = ^TLinkedListBase; + + {** + @abstract Base record for a managed linked list. + @field Count Number of elements in the list. + @field Head Pointer to the first node. + @field ElementSize Size (in bytes) of each element. + **} + TLinkedListBase = record + Count : uint32; + Head : PLinkedList; + ElementSize : uint32; + end; + + { ------ Shared node for FIFO & LIFO ------ } {** diff --git a/src/core/strings/core.strings.pas b/src/core/strings/core.strings.pas index 8c46a43b..565d4aac 100644 --- a/src/core/strings/core.strings.pas +++ b/src/core/strings/core.strings.pas @@ -1,4 +1,5 @@ // Copyright 2021 Kieron Morris +// Copyright 2026 Aaron Hance // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -23,9 +24,7 @@ unit core.strings; interface uses - core.util, arch.x86.util, - memory.heap, - core.ds.lists; + core.util, memory.heap; function charToLower(c : char) : char; function charToUpper(c : char) : char; @@ -62,7 +61,7 @@ procedure UnitTest; implementation uses - io.syslog; + arch.platform.diagnostics; function hexStringToInt(str : pchar) : uint32; var @@ -160,7 +159,8 @@ var begin size:= stringSize(str); result:= stringNew(size); - memcpy(uint32(str), uint32(result), size); + if result = nil then begin stringCopy:= nil; exit; end; + memcpy(PtrUInt(str), PtrUInt(result), size); stringCopy:= result; end; @@ -170,9 +170,12 @@ var newSize : uint32; begin + stringNew:= nil; + if size = high(uint32) then exit; newSize:= size + 1; result:= pchar(kalloc(newSize)); - memset(uint32(result), 0, newSize); + if result = nil then exit; + memset(PtrUInt(result), 0, newSize); stringNew:= result; end; @@ -200,9 +203,11 @@ var begin size1:= stringSize(str1); size2:= stringSize(str2); + if size2 >= high(uint32) - size1 then begin stringConcat:= nil; exit; end; result:= stringNew(size1 + size2); - memcpy(uint32(str1), uint32(result), size1); - memcpy(uint32(str2), uint32(result + size1), size2); + if result = nil then begin stringConcat:= nil; exit; end; + memcpy(PtrUInt(str1), PtrUInt(result), size1); + memcpy(PtrUInt(str2), PtrUInt(result + size1), size2); stringConcat:= result; end; @@ -215,7 +220,8 @@ begin strLen:= stringSize(str); if length > strLen then length:= strLen; result:= stringNew(length); - memcpy(uint32(str), uint32(result), length); + if result = nil then begin stringTrim:= nil; exit; end; + memcpy(PtrUInt(str), PtrUInt(result), length); stringTrim:= result; end; @@ -230,9 +236,10 @@ begin stringSub:= stringNew(0); exit; end; - if start + size > strLen then size:= strLen - start; + if size > strLen - start then size:= strLen - start; result:= stringNew(size); - memcpy(uint32(str)+start, uint32(result), size); + if result = nil then begin stringSub:= nil; exit; end; + memcpy(PtrUInt(str)+start, PtrUInt(result), size); stringSub:= result; end; @@ -248,7 +255,8 @@ begin stringMatchAt:= true; exit; end; - if pos + findLen > strLen then exit; + if pos > strLen then exit; + if findLen > strLen - pos then exit; for i:= 0 to findLen - 1 do begin if str[pos + i] <> find[i] then exit; end; @@ -279,17 +287,19 @@ begin // If we found the find string, replace it. if found then begin + if replLen >= high(uint32) - (strLen - findLen) then begin stringReplace:= nil; exit; end; result:= stringNew(strLen - findLen + replLen); + if result = nil then begin stringReplace:= nil; exit; end; // Copy prefix before match if pos > 0 then - memcpy(uint32(str), uint32(result), pos); + memcpy(PtrUInt(str), PtrUInt(result), pos); // Copy replacement if replLen > 0 then - memcpy(uint32(replace), uint32(result + pos), replLen); + memcpy(PtrUInt(replace), PtrUInt(result + pos), replLen); // Copy suffix after match suffixLen:= strLen - pos - findLen; if suffixLen > 0 then - memcpy(uint32(str) + pos + findLen, uint32(result) + pos + replLen, suffixLen); + memcpy(PtrUInt(str) + pos + findLen, PtrUInt(result) + pos + replLen, suffixLen); stringReplace:= result; end else begin stringReplace:= stringCopy(str); @@ -555,7 +565,7 @@ end; function int64ToString(i : int64) : pchar; var result : pchar; - tmp : int64; + tmp, magnitude : uint64; len : uint32; pos : uint32; neg : boolean; @@ -568,10 +578,11 @@ begin exit; end; neg:= (i < 0); - if neg then i:= -i; + if neg then magnitude:= uint64(-(i + 1)) + 1 + else magnitude:= uint64(i); { Count digits } len:= 0; - tmp:= i; + tmp:= magnitude; while tmp > 0 do begin inc(len); tmp:= tmp div 10; @@ -580,7 +591,7 @@ begin { Build string from right to left } result:= stringNew(len); pos:= len; - tmp:= i; + tmp:= magnitude; while tmp > 0 do begin dec(pos); result[pos]:= char((tmp mod 10) + 48); @@ -755,32 +766,16 @@ var end else begin inc(failed); msg:= stringConcat('FAIL: ', testName); - io.syslog.logln('STRINGS', msg); + arch.platform.diagnostics.log('STRINGS', msg); kfree(void(msg)); end; end; - procedure PrintSummary; - var - pStr, fStr, msg, tmp : pchar; - begin - pStr:= intToString(passed); - fStr:= intToString(failed); - msg:= stringConcat(pStr, ' passed, '); - tmp:= stringConcat(msg, fStr); - kfree(void(msg)); - msg:= stringConcat(tmp, ' failed.'); - kfree(void(tmp)); - io.syslog.logln('STRINGS', msg); - kfree(void(msg)); - kfree(void(pStr)); - kfree(void(fStr)); - end; begin passed:= 0; failed:= 0; - io.syslog.logln('STRINGS', 'Unit tests starting...'); + arch.platform.diagnostics.log('STRINGS', 'Unit tests starting...'); { === stringSize === } Assert(stringSize('HELLO') = 5, 'stringSize(HELLO)=5'); @@ -789,6 +784,7 @@ begin Assert(stringSize(nil) = 0, 'stringSize(nil)=0'); { === stringNew === } + Assert(stringNew(high(uint32)) = nil, 'stringNew rejects terminator overflow'); s:= stringNew(0); Assert(stringSize(s) = 0, 'stringNew(0) is empty'); kfree(void(s)); @@ -1019,6 +1015,15 @@ begin Assert(stringEquals(s, '1000000'), 'int64ToString large'); kfree(void(s)); + s:= int64ToString(low(int64)); + Assert(stringEquals(s, '-9223372036854775808'), 'int64ToString minimum'); + kfree(void(s)); + s:= stringSub('ABC', 1, high(uint32)); + Assert(stringEquals(s, 'BC'), 'substring size cannot wrap'); + kfree(void(s)); + Assert(not stringMatchAt('ABC', high(uint32), 'BC'), 'match position cannot wrap'); + Assert((Trunc(-3.75) = -3) and (Trunc(3.75) = 3), 'Trunc rounds toward zero'); + { === doubleToStr === } s:= doubleToStr(0.0); Assert(stringEquals(s, '0'), 'doubleToStr zero'); @@ -1114,7 +1119,8 @@ begin Assert(stringEndsWithCI('Hello', 'xyz') = false, 'stringEndsWithCI no match'); { Print summary } - PrintSummary; + + arch.platform.diagnostics.testSummary('STRINGS', 'STRINGS_TESTS_PASSED', 'STRINGS_TESTS_FAILED', passed, failed); end; end. \ No newline at end of file diff --git a/src/debug/debug.early.pas b/src/debug/debug.early.pas new file mode 100644 index 00000000..8f64f664 --- /dev/null +++ b/src/debug/debug.early.pas @@ -0,0 +1,55 @@ +// Copyright 2026 Aaron Hance +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +{ Allocation-free diagnostics. The architecture supplies the character sink; + formatting remains independent of UARTs, graphics, heaps, and interrupts. } +unit debug.early; +interface +type + TCharWriter = procedure(ch: char); +procedure setWriter(writer: TCharWriter); +procedure puts(message: pchar); +procedure hex(value: uint64); +procedure field(name: pchar; value: uint64); +implementation +var + sink: TCharWriter; +procedure setWriter(writer: TCharWriter); +begin + sink := writer; +end; +procedure puts(message: pchar); +begin + if sink = nil then exit; + while message^ <> #0 do begin + sink(message^); + inc(message); + end; +end; +procedure hex(value: uint64); +const + digits: array[0..15] of char = '0123456789ABCDEF'; +var + shift: sint32; +begin + if sink = nil then exit; + for shift := 15 downto 0 do + sink(digits[(value shr (shift * 4)) and $F]); +end; +procedure field(name: pchar; value: uint64); +begin + puts(name); + puts('='); + hex(value); + puts(#13#10); +end; +end. diff --git a/src/driver/storage/driver.storage.fdtable.pas b/src/driver/storage/driver.storage.fdtable.pas index 50430d40..a61b88b0 100644 --- a/src/driver/storage/driver.storage.fdtable.pas +++ b/src/driver/storage/driver.storage.fdtable.pas @@ -1,4 +1,5 @@ // Copyright 2021 Aaron Hance +// Copyright 2026 Aaron Hance // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -28,15 +29,18 @@ interface uses memory.heap, driver.storage.types, - core.util, arch.x86.util; + memory.raw, arch.common.interrupts; const MAX_FDS = 64; type + TFDIdleHook = procedure; PFileDescriptor = ^TFileDescriptor; TFileDescriptor = record InUse : boolean; + Closing : boolean; + WaitClose : boolean; AsyncRefs : uint32; { in-flight async ops using this descriptor } Volume : PStorage_Volume; Directory : pchar; { directory path within the volume } @@ -53,6 +57,7 @@ type PFDTable = ^TFDTable; TFDTable = record Entries : array[0..MAX_FDS-1] of TFileDescriptor; + Closing, Freeing: boolean; end; { Allocate and zero-initialise a new FD table on the heap. @@ -61,8 +66,11 @@ type function fd_table_new : PFDTable; { Free all open descriptors in the table, then free the table itself. - Called when a process is reaped. Safe to pass nil. } + Called when a process is reaped. Safe to pass nil. If begin_close fails, + ownership remains with the caller until async users release their references. } procedure fd_table_free(table : PFDTable); +{ Atomically prevent new references before detaching an owner. False means busy. } +function fd_table_begin_close(table: PFDTable): boolean; { Find the first free slot in the table. Returns 1-based handle (1..MAX_FDS) on success, 0 if table is full. } @@ -88,7 +96,20 @@ procedure fd_close_all(table : PFDTable); Called when a volume is removed to prevent dangling pointers. } procedure fd_close_for_volume(table : PFDTable; vol : PStorage_Volume); +procedure UnitTest; +{ Hook only wakes a worker; called with IRQs excluded, never performs cleanup. } +procedure fd_set_idle_hook(hook: TFDIdleHook); + implementation +uses debug.early; +var idleHook: TFDIdleHook; +procedure fd_set_idle_hook(hook: TFDIdleHook); +var flags: PtrUInt; +begin + flags := arch.common.interrupts.lock; + idleHook := hook; + arch.common.interrupts.unlock(flags); +end; function fd_table_new : PFDTable; var @@ -96,49 +117,75 @@ var begin tbl := PFDTable(kalloc(SizeOf(TFDTable))); if tbl <> nil then - memset(uint32(tbl), 0, SizeOf(TFDTable)); + memory.raw.fill(pointer(tbl), 0, SizeOf(TFDTable)); fd_table_new := tbl; end; type TFSPrivateClose = procedure(data : pointer); -procedure fd_free_entry(entry : PFileDescriptor); +procedure releaseEntry(entry: PFileDescriptor); +var data: pointer; hook: TFSPrivateClose; flags: PtrUInt; wake: boolean; begin - if entry = nil then exit; - if not entry^.InUse then exit; - - { Free FS-level per-file metadata first } - if (entry^.FSPrivate <> nil) and (entry^.FSCloseHook <> nil) then - TFSPrivateClose(entry^.FSCloseHook)(entry^.FSPrivate); - entry^.FSPrivate := nil; - entry^.FSCloseHook := nil; - - if entry^.Directory <> nil then begin - kfree(void(entry^.Directory)); - entry^.Directory := nil; - end; - if entry^.FileName <> nil then begin - kfree(void(entry^.FileName)); - entry^.FileName := nil; - end; - if entry^.VFSDir <> nil then begin - kfree(void(entry^.VFSDir)); - entry^.VFSDir := nil; - end; - - entry^.InUse := false; - entry^.AsyncRefs := 0; - entry^.DataSize := 0; - entry^.DeviceOps := nil; - entry^.DeviceData := nil; + { Entry is already claimed under IRQ exclusion. Never publish it for reuse + while its close callback can still re-enter the descriptor API. } + data := entry^.FSPrivate; hook := TFSPrivateClose(entry^.FSCloseHook); + entry^.FSPrivate := nil; entry^.FSCloseHook := nil; + if (data <> nil) and assigned(hook) then hook(data); + if entry^.Directory <> nil then kfree(void(entry^.Directory)); + if entry^.FileName <> nil then kfree(void(entry^.FileName)); + if entry^.VFSDir <> nil then kfree(void(entry^.VFSDir)); + flags := arch.common.interrupts.lock; + wake := entry^.WaitClose; + memory.raw.fill(pointer(entry), 0, sizeof(TFileDescriptor)); + if wake and assigned(idleHook) then idleHook; + arch.common.interrupts.unlock(flags); end; - -procedure fd_table_free(table : PFDTable); +function fd_free_entry(entry: PFileDescriptor): boolean; +var flags: PtrUInt; +begin + fd_free_entry := false; + if entry = nil then exit; + flags := arch.common.interrupts.lock; + if (not entry^.InUse) or entry^.Closing or (entry^.AsyncRefs <> 0) then begin + arch.common.interrupts.unlock(flags); exit; + end; + entry^.Closing := true; + arch.common.interrupts.unlock(flags); + releaseEntry(entry); + fd_free_entry := true; +end; +function fd_table_begin_close(table: PFDTable): boolean; +var flags: PtrUInt; i: uint32; +begin + fd_table_begin_close := true; + if table = nil then exit; + flags := arch.common.interrupts.lock; + if not table^.Closing then begin + for i := 0 to MAX_FDS - 1 do + if table^.Entries[i].Closing or (table^.Entries[i].AsyncRefs <> 0) then begin + table^.Entries[i].WaitClose := true; + fd_table_begin_close := false; + end; + if not fd_table_begin_close then begin arch.common.interrupts.unlock(flags); exit; end; + table^.Closing := true; + for i := 0 to MAX_FDS - 1 do table^.Entries[i].Closing := true; + end; + arch.common.interrupts.unlock(flags); +end; +procedure fd_table_free(table: PFDTable); +var flags: PtrUInt; i: uint32; begin if table = nil then exit; - fd_close_all(table); - kfree(puint32(table)); + flags := arch.common.interrupts.lock; + if table^.Freeing or not fd_table_begin_close(table) then begin + arch.common.interrupts.unlock(flags); exit; + end; + table^.Freeing := true; + arch.common.interrupts.unlock(flags); + for i := 0 to MAX_FDS - 1 do + if table^.Entries[i].InUse then releaseEntry(@table^.Entries[i]); + kfree(void(table)); end; function fd_alloc(table : PFDTable) : uint32; @@ -147,8 +194,9 @@ var begin fd_alloc := 0; if table = nil then exit; + if table^.Closing then exit; for i := 0 to MAX_FDS - 1 do begin - if not table^.Entries[i].InUse then begin + if (not table^.Entries[i].InUse) and (not table^.Entries[i].Closing) then begin fd_alloc := i + 1; { 1-based } exit; end; @@ -160,36 +208,41 @@ begin fd_get := nil; if table = nil then exit; if (handle = 0) or (handle > MAX_FDS) then exit; + if table^.Closing or table^.Entries[handle - 1].Closing then exit; if not table^.Entries[handle - 1].InUse then exit; fd_get := @table^.Entries[handle - 1]; end; function fd_begin_async(entry : PFileDescriptor) : boolean; +var flags: PtrUInt; begin fd_begin_async := false; if entry = nil then exit; - asm pushf; cli end; - if entry^.InUse then begin + flags := arch.common.interrupts.lock; + if entry^.InUse and not entry^.Closing and (entry^.AsyncRefs < High(uint32)) then begin entry^.AsyncRefs := entry^.AsyncRefs + 1; fd_begin_async := true; end; - asm popf end; + arch.common.interrupts.unlock(flags); end; procedure fd_end_async(entry : PFileDescriptor); +var flags: PtrUInt; begin if entry = nil then exit; - asm pushf; cli end; - if entry^.AsyncRefs > 0 then - entry^.AsyncRefs := entry^.AsyncRefs - 1; - asm popf end; + flags := arch.common.interrupts.lock; + if entry^.AsyncRefs > 0 then begin + dec(entry^.AsyncRefs); + if (entry^.AsyncRefs = 0) and entry^.WaitClose and assigned(idleHook) then idleHook; + end; + arch.common.interrupts.unlock(flags); end; function fd_has_async(entry : PFileDescriptor) : boolean; begin fd_has_async := false; if entry = nil then exit; - fd_has_async := entry^.AsyncRefs > 0; + fd_has_async := PUInt32(@entry^.AsyncRefs)^ > 0; end; function fd_close(table : PFDTable; handle : uint32) : boolean; @@ -197,9 +250,9 @@ begin fd_close := false; if table = nil then exit; if (handle = 0) or (handle > MAX_FDS) then exit; + if table^.Closing or table^.Entries[handle - 1].Closing then exit; if not table^.Entries[handle - 1].InUse then exit; - fd_free_entry(@table^.Entries[handle - 1]); - fd_close := true; + fd_close := fd_free_entry(@table^.Entries[handle - 1]); end; procedure fd_close_all(table : PFDTable); @@ -225,4 +278,63 @@ begin end; end; +var + testTable: PFDTable; + testHandle, testClosed: uint32; + testReentry: boolean; +procedure testClose(data: pointer); +begin + inc(testClosed); + testReentry := not fd_close(testTable, testHandle); + kfree(void(data)); +end; +procedure UnitTest; +var + table: PFDTable; + entry: PFileDescriptor; + i, passed, failed: uint32; + beforeFree: SizeUInt; + slotsValid: boolean; + procedure Assert(condition: boolean; name: pchar); + begin + if condition then begin inc(passed); puts('PASS: '); end + else begin inc(failed); puts('FAIL: '); end; + puts(name); puts(#13#10); + end; +begin + passed := 0; failed := 0; beforeFree := lmm_total_free; + table := fd_table_new; + testTable := table; testHandle := 1; testClosed := 0; testReentry := false; + Assert((fd_get(table, 0) = nil) and (fd_get(table, MAX_FDS + 1) = nil), 'FD handle boundaries'); + slotsValid := true; + for i := 0 to MAX_FDS - 1 do begin + if fd_alloc(table) <> i + 1 then slotsValid := false; + table^.Entries[i].InUse := true; + end; + Assert(slotsValid, 'FD bounded slot allocation'); + Assert(fd_alloc(table) = 0, 'FD full table rejects allocation'); + entry := fd_get(table, 1); + entry^.Directory := pchar(kalloc(3)); entry^.FileName := pchar(kalloc(5)); + entry^.VFSDir := pchar(kalloc(7)); entry^.FSPrivate := pointer(kalloc(9)); + entry^.FSCloseHook := @testClose; + Assert(fd_begin_async(entry), 'FD acquires async reference'); + Assert(not fd_close(table, 1), 'FD close retains active async storage'); + Assert(not fd_table_begin_close(table), 'FD table teardown waits for async references'); + fd_end_async(entry); + Assert(not fd_has_async(entry), 'FD completion releases async reference'); + Assert(fd_close(table, 1), 'FD closes after async completion'); + Assert((testClosed = 1) and testReentry, 'FD callback reentry cannot close twice'); + Assert(fd_get(table, 1) = nil, 'FD closed handle unavailable'); + Assert(fd_alloc(table) = 1, 'FD closed slot reusable'); + entry := @table^.Entries[0]; entry^.InUse := true; + entry^.AsyncRefs := High(uint32); + Assert(not fd_begin_async(entry), 'FD async reference overflow rejected'); + entry^.AsyncRefs := 0; + Assert(fd_table_begin_close(table), 'FD table teardown reserves all slots'); + Assert((fd_alloc(table) = 0) and not fd_begin_async(entry), 'FD closing table rejects new users'); + fd_table_free(table); + Assert(lmm_total_free = beforeFree, 'FD table strings and metadata released'); + field('FD_TESTS_PASSED', passed); field('FD_TESTS_FAILED', failed); +end; + end. diff --git a/src/driver/storage/driver.storage.types.pas b/src/driver/storage/driver.storage.types.pas index 283b07e3..6080018a 100644 --- a/src/driver/storage/driver.storage.types.pas +++ b/src/driver/storage/driver.storage.types.pas @@ -1,4 +1,5 @@ // Copyright 2021 Aaron Hance +// Copyright 2026 Aaron Hance // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -23,7 +24,7 @@ interface uses core.ds.types, - core.ds.lists; + core.ds.dynamic; type diff --git a/src/io/io.directory.pas b/src/io/io.directory.pas new file mode 100644 index 00000000..2e7dc454 --- /dev/null +++ b/src/io/io.directory.pas @@ -0,0 +1,31 @@ +// Copyright 2026 Aaron Hance +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +{ VFS directory helpers extracted from io.stdio (Kieron Morris). } +unit io.directory; +interface +function getWorkingDirectory: pchar; +procedure setWorkingDirectory(str: pchar); +implementation +uses driver.storage.vfs; +function getWorkingDirectory: pchar; +begin + getWorkingDirectory := driver.storage.vfs.GetWorkingDirectory; +end; + +procedure setWorkingDirectory(str: pchar); +begin + if str <> nil then + driver.storage.vfs.ChangeDirectory(str); +end; + +end. diff --git a/src/io/io.stdio.pas b/src/io/io.stdio.pas index e90cd5b5..64090597 100644 --- a/src/io/io.stdio.pas +++ b/src/io/io.stdio.pas @@ -1,3 +1,15 @@ +// Copyright 2026 Aaron Hance +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + { Stdio - Standard I/O abstraction for command-line programs. @@ -15,43 +27,18 @@ unit io.stdio; interface uses - boot.mgr, - memory.heap, core.strings, core.util, arch.x86.util, debug.tracer; + io.types, memory.heap, core.strings, core.util; type - { Output buffer — kalloc'd, growable } - POutBuf = ^TOutBuf; - TOutBuf = record - buf : pchar; - len : uint32; - cap : uint32; - end; - - { Parameter linked list } - PParamList = ^TParamList; - TParamList = record - Param : pchar; - Next : PParamList; - end; - - { Command buffer type } - TCommandBuffer = array[0..1023] of byte; - - { Command method — receives params plus stdin (read), stdout (write), stderr (write) } - TCommandMethod = procedure(params: PParamList; stdin_buf, stdout_buf, stderr_buf: POutBuf); - - { Command record } - PCommand = ^TCommand; - TCommand = record - registered : boolean; - hidden : boolean; - command : pchar; - method : TCommandMethod; - description : pchar; - end; - - { Halt callback } - THaltCallback = procedure(); + POutBuf = io.types.POutBuf; + TOutBuf = io.types.TOutBuf; + PParamList = io.types.PParamList; + TParamList = io.types.TParamList; + TCommandBuffer = io.types.TCommandBuffer; + TCommandMethod = io.types.TCommandMethod; + PCommand = io.types.PCommand; + TCommand = io.types.TCommand; + THaltCallback = io.types.THaltCallback; { Output buffer management } function createOutBuf(initial_cap: uint32): POutBuf; @@ -92,9 +79,7 @@ procedure freeParams(params: PParamList); function paramCount(params: PParamList): uint32; function getParam(index: uint32; params: PParamList): pchar; -{ Working directory } -function getWorkingDirectory: pchar; -procedure setWorkingDirectory(str: pchar); +{ Working-directory helpers live in io.directory, which requires VFS. } { Halt mechanism for async commands } function halt(id: uint32; cb: THaltCallback): boolean; @@ -107,11 +92,12 @@ var { Initialization } procedure init; +procedure UnitTest; implementation uses - core.version, driver.timer.rtc, driver.io.serial, driver.storage.vfs, io.syslog; + arch.platform.diagnostics, arch.common.interrupts; const INITIAL_CMD_CAP = 64; @@ -130,10 +116,13 @@ var begin if initial_cap = 0 then cap := 1 else cap := initial_cap; ob := POutBuf(kalloc(SizeOf(TOutBuf))); + createOutBuf := nil; + if ob = nil then exit; ob^.buf := pchar(kalloc(cap)); + if ob^.buf = nil then begin kfree(void(ob)); exit; end; ob^.len := 0; ob^.cap := cap; - memset(uint32(ob^.buf), 0, cap); + memset(PtrUInt(ob^.buf), 0, cap); createOutBuf := ob; end; @@ -152,44 +141,53 @@ begin buf^.buf[0] := #0; end; -procedure bufGrow(buf: POutBuf; needed: uint32); -var - newCap: uint32; - newBuf: pchar; +function bufGrow(buf: POutBuf; needed: uint32): boolean; +var newCap, required: uint32; newBuf: pchar; begin - if buf^.len + needed < buf^.cap then exit; + bufGrow := false; + if needed >= high(uint32) - buf^.len then exit; + required := buf^.len + needed + 1; + if required <= buf^.cap then begin bufGrow := true; exit; end; newCap := buf^.cap; if newCap = 0 then newCap := 64; - while newCap <= buf^.len + needed do + while newCap < required do begin + if newCap > high(uint32) div 2 then begin newCap := required; break; end; newCap := newCap * 2; + end; newBuf := pchar(kalloc(newCap)); - memset(uint32(newBuf), 0, newCap); - if buf^.len > 0 then - memcpy(uint32(buf^.buf), uint32(newBuf), buf^.len); + if newBuf = nil then exit; + memcpy(PtrUInt(buf^.buf), PtrUInt(newBuf), buf^.len); + newBuf[buf^.len] := #0; kfree(void(buf^.buf)); buf^.buf := newBuf; buf^.cap := newCap; + bufGrow := true; end; procedure bufWriteChar(buf: POutBuf; c: char); begin if buf = nil then exit; - bufGrow(buf, 2); + if not bufGrow(buf, 1) then exit; buf^.buf[buf^.len] := c; Inc(buf^.len); buf^.buf[buf^.len] := #0; end; procedure bufWriteStr(buf: POutBuf; s: pchar); -var - i: uint32; +var len, offset: uint32; aliases: boolean; begin if (buf = nil) or (s = nil) then exit; - i := 0; - while s[i] <> #0 do begin - bufWriteChar(buf, s[i]); - Inc(i); - end; + len := stringSize(s); + { A source inside the buffer must follow it if growth relocates storage. } + aliases := (PtrUInt(s) >= PtrUInt(buf^.buf)) and + (PtrUInt(s) - PtrUInt(buf^.buf) < buf^.cap); + offset := 0; + if aliases then offset := PtrUInt(s) - PtrUInt(buf^.buf); + if not bufGrow(buf, len) then exit; + if aliases then s := buf^.buf + offset; + memcpy(PtrUInt(s), PtrUInt(buf^.buf + buf^.len), len); + inc(buf^.len, len); + buf^.buf[buf^.len] := #0; end; procedure bufWriteStrLn(buf: POutBuf; s: pchar); @@ -333,28 +331,24 @@ end; { ---- Dynamic command registry ---- } -procedure growCommands; -var - newCap: uint32; - newArr: PCommand; - i: uint32; +function growCommands: boolean; +var newCap: uint32; newArr: PCommand; begin - newCap := CmdCap * 2; - newArr := PCommand(kalloc(newCap * SizeOf(TCommand))); - memset(uint32(newArr), 0, newCap * SizeOf(TCommand)); - { Copy existing } - for i := 0 to CmdCap - 1 do begin - PCommand(uint32(newArr) + i * SizeOf(TCommand))^ := - PCommand(uint32(Commands) + i * SizeOf(TCommand))^; - end; + growCommands := false; + if CmdCap > high(uint32) div sizeof(TCommand) div 2 then exit; + if CmdCap = 0 then newCap := INITIAL_CMD_CAP else newCap := CmdCap * 2; + newArr := PCommand(kalloc(newCap * sizeof(TCommand))); + if newArr = nil then exit; + memcpy(PtrUInt(Commands), PtrUInt(newArr), CmdCount * sizeof(TCommand)); kfree(void(Commands)); Commands := newArr; CmdCap := newCap; + growCommands := true; end; function getCmdPtr(index: uint32): PCommand; inline; begin - getCmdPtr := PCommand(uint32(Commands) + index * SizeOf(TCommand)); + getCmdPtr := PCommand(PtrUInt(Commands) + index * SizeOf(TCommand)); end; procedure registerCommand(command: pchar; method: TCommandMethod; description: pchar); @@ -366,7 +360,8 @@ procedure registerCommandEx(command: pchar; method: TCommandMethod; description: var cmd: PCommand; begin - if CmdCount >= CmdCap then growCommands; + if (command = nil) or (method = nil) then exit; + if CmdCount >= CmdCap then if not growCommands then exit; cmd := getCmdPtr(CmdCount); cmd^.registered := true; cmd^.hidden := hide; @@ -390,61 +385,49 @@ begin end; function findCommand(name: pchar): PCommand; -var - i: uint32; - cmd: PCommand; - ua, ub: pchar; +var i: uint32; cmd: PCommand; begin findCommand := nil; - ua := stringToUpper(name); + if (name = nil) or (CmdCount = 0) then exit; for i := 0 to CmdCount - 1 do begin cmd := getCmdPtr(i); - if cmd^.registered then begin - ub := stringToUpper(cmd^.command); - if stringEquals(ua, ub) then begin - kfree(void(ub)); - kfree(void(ua)); - findCommand := cmd; - exit; - end; - kfree(void(ub)); + if cmd^.registered and stringEqualsCI(name, cmd^.command) then begin + findCommand := cmd; + exit; end; end; - kfree(void(ua)); end; { ---- Parameter parsing ---- } function getParams(var buf: TCommandBuffer): PParamList; var - start, finish: uint32; - size: uint32; - ptr: uint32; - root: PParamList; - current: PParamList; + start, finish, size: uint32; + root, current: PParamList; begin + getParams := nil; root := PParamList(kalloc(SizeOf(TParamList))); + if root = nil then exit; current := root; current^.next := nil; current^.Param := nil; - start := 0; finish := 0; - while buf[start] <> 0 do begin - while (char(buf[finish]) <> ' ') and (buf[finish] <> 0) do - Inc(finish); + while finish < sizeof(buf) do begin + while (finish < sizeof(buf)) and (buf[finish] = ord(' ')) do inc(finish); + if finish = sizeof(buf) then break; + if buf[finish] = 0 then break; + start := finish; + while (finish < sizeof(buf)) and (buf[finish] <> ord(' ')) and + (buf[finish] <> 0) do inc(finish); size := finish - start; - if size > 0 then begin - ptr := uint32(@buf[start]); - current^.Param := pchar(kalloc(size + 2)); - memset(uint32(current^.Param), 0, size + 2); - memcpy(uint32(ptr), uint32(current^.Param), size); - current^.next := PParamList(kalloc(SizeOf(TParamList))); - current := current^.next; - current^.next := nil; - current^.Param := nil; - end; - start := finish + 1; - Inc(finish); + current^.Param := stringNew(size); + if current^.Param = nil then begin freeParams(root); exit; end; + memcpy(PtrUInt(@buf[start]), PtrUInt(current^.Param), size); + current^.next := PParamList(kalloc(SizeOf(TParamList))); + if current^.next = nil then begin freeParams(root); exit; end; + current := current^.next; + current^.next := nil; + current^.Param := nil; end; getParams := root; end; @@ -499,78 +482,46 @@ begin getParam := search^.param; end; -{ ---- Working directory ---- } - -function getWorkingDirectory: pchar; -begin - getWorkingDirectory := driver.storage.vfs.GetWorkingDirectory; -end; - -procedure setWorkingDirectory(str: pchar); -begin - if str <> nil then - driver.storage.vfs.ChangeDirectory(str); -end; { ---- Halt mechanism ---- } function halt(id: uint32; cb: THaltCallback): boolean; +var flags: PtrUInt; begin - halt := false; + flags := arch.common.interrupts.lock; + halt := not Halted; if not Halted then begin - Halted := true; - halt := true; HaltID := id; HaltCB := cb; + Halted := true; end; + arch.common.interrupts.unlock(flags); end; function done(id: uint32): boolean; +var flags: PtrUInt; callback: THaltCallback; begin - done := false; - if Halted then begin - if id = HaltID then begin - if HaltCB <> nil then HaltCB(); - HaltCB := nil; - Halted := false; - HaltID := 0; - done := true; - end; + flags := arch.common.interrupts.lock; + done := Halted and (id = HaltID); + callback := nil; + if done then begin + callback := HaltCB; + HaltCB := nil; + Halted := false; + HaltID := 0; end; + arch.common.interrupts.unlock(flags); + if callback <> nil then callback(); end; { ---- Built-in commands ---- } -procedure cmd_version(params: PParamList; stdin_buf, stdout_buf, stderr_buf: POutBuf); -begin - bufWriteStr(stdout_buf, ' Asuro Version: '); - bufWriteStrLn(stdout_buf, core.version.VERSION); - bufWriteStr(stdout_buf, ' Compiled on: '); - bufWriteStr(stdout_buf, core.version.COMPILE_DATE); - bufWriteStr(stdout_buf, ' '); - bufWriteStrLn(stdout_buf, core.version.COMPILE_TIME); - bufWriteStrLn(stdout_buf, ' Compiled With: '); - bufWriteStr(stdout_buf, ' NASM - Version: '); - bufWriteStrLn(stdout_buf, core.version.NASM_VERSION); - bufWriteStr(stdout_buf, ' FPC - Version: '); - bufWriteStrLn(stdout_buf, core.version.FPC_VERSION); - bufWriteStr(stdout_buf, ' MAKE - Version: '); - bufWriteStrLn(stdout_buf, core.version.MAKE_VERSION); - bufWriteStr(stdout_buf, ' '); - bufWriteInt(stdout_buf, core.version.LINE_COUNT); - bufWriteStr(stdout_buf, ' lines, across '); - bufWriteInt(stdout_buf, core.version.FILE_COUNT); - bufWriteStrLn(stdout_buf, ' files.'); - bufWriteStr(stdout_buf, ' Baked Drivers: '); - bufWriteIntLn(stdout_buf, core.version.DRIVER_COUNT); - bufWriteStr(stdout_buf, ' Checksum: '); - bufWriteStrLn(stdout_buf, core.version.CHECKSUM); -end; procedure cmd_echo(params: PParamList; stdin_buf, stdout_buf, stderr_buf: POutBuf); var current: PParamList; begin + if params = nil then exit; current := params^.next; while (current <> nil) and (current^.param <> nil) do begin bufWriteStr(stdout_buf, current^.param); @@ -596,6 +547,7 @@ var len: uint32; cmd: PCommand; begin + if CmdCount = 0 then exit; longestCommand := 0; for i := 0 to CmdCount - 1 do begin cmd := getCmdPtr(i); @@ -619,63 +571,114 @@ begin end; end; -procedure cmd_time(params: PParamList; stdin_buf, stdout_buf, stderr_buf: POutBuf); -var - dt: TDateTime; + +var testCallbackCount: uint32; + testCallbackReentered: boolean; +procedure testHaltCallback; begin - dt := getDateTime; - if dt.Day < 10 then bufWriteChar(stdout_buf, '0'); - bufWriteInt(stdout_buf, dt.Day); - bufWriteChar(stdout_buf, '/'); - if dt.Month < 10 then bufWriteChar(stdout_buf, '0'); - bufWriteInt(stdout_buf, dt.Month); - bufWriteChar(stdout_buf, '/'); - bufWriteInt(stdout_buf, dt.Century); - bufWriteInt(stdout_buf, dt.Year); - bufWriteChar(stdout_buf, ' '); - if dt.Hours < 10 then bufWriteChar(stdout_buf, '0'); - bufWriteInt(stdout_buf, dt.Hours); - bufWriteChar(stdout_buf, ':'); - if dt.Minutes < 10 then bufWriteChar(stdout_buf, '0'); - bufWriteInt(stdout_buf, dt.Minutes); - bufWriteChar(stdout_buf, ':'); - if dt.Seconds < 10 then bufWriteChar(stdout_buf, '0'); - bufWriteIntLn(stdout_buf, dt.Seconds); - bufWriteStr(stdout_buf, 'Weekday: '); - bufWriteStrLn(stdout_buf, WeekdayToString(dt.Weekday)); + inc(testCallbackCount); + testCallbackReentered := (not done(41)) and halt(42, nil); end; -procedure cmd_reboot(params: PParamList; stdin_buf, stdout_buf, stderr_buf: POutBuf); +{ Tests use an isolated command registry; production registrations are restored. } +procedure UnitTest; +type PCommandBuffer = ^TCommandBuffer; +var + input: PCommandBuffer; + params: PParamList; + output: POutBuf; + oldCommands: PCommand; + oldHalted: boolean; + oldHaltID: uint32; + oldHaltCB: THaltCallback; + oldCount, oldCap, i, passed, failed: uint32; + beforeFree: SizeUInt; + procedure Assert(condition: boolean; name: pchar); + begin + if condition then inc(passed) else begin + inc(failed); + arch.platform.diagnostics.log('STDIO FAIL', name); + end; + end; begin - resetSystem; + passed := 0; failed := 0; beforeFree := lmm_total_free; + oldHalted := Halted; oldHaltID := HaltID; oldHaltCB := HaltCB; + Halted := false; HaltID := 0; HaltCB := nil; + testCallbackCount := 0; testCallbackReentered := false; + Assert(halt(41, @testHaltCallback) and not halt(42, nil), 'halt reserves one owner'); + Assert(not done(42) and Halted, 'wrong completion preserves halt owner'); + Assert(done(41) and (testCallbackCount = 1) and testCallbackReentered and + Halted and (HaltID = 42), 'completion detaches before callback reentry'); + Assert(done(42) and not done(42), 'completion releases exactly once'); + Halted := oldHalted; HaltID := oldHaltID; HaltCB := oldHaltCB; + oldCommands := Commands; oldCount := CmdCount; oldCap := CmdCap; + Commands := nil; CmdCount := 0; CmdCap := 0; + Assert(findCommand('missing') = nil, 'empty registry lookup'); + for i := 0 to 69 do registerCommand('ECHO', @cmd_echo, 'test'); + Assert((getCommandCount = 70) and (getCommand(69) <> nil) and + (getCommand(70) = nil), 'registry growth and bounds'); + Assert(findCommand('echo') = getCommand(0), 'case-insensitive command lookup'); + output := createOutBuf(1); + input := PCommandBuffer(kalloc(sizeof(TCommandBuffer))); + if (output <> nil) and (input <> nil) then begin + bufWriteStr(output, 'grow'); + bufWriteStr(output, output^.buf); + Assert(stringEquals(output^.buf, 'growgrow'), 'self append across buffer relocation'); + bufClear(output); + bufWriteInt(output, low(integer)); + Assert(stringEquals(output^.buf, '-2147483648'), 'signed integer formatting'); + bufClear(output); + bufWriteHex(output, $89ABCDEF); + Assert(stringEquals(output^.buf, '0x89ABCDEF'), 'hexadecimal formatting'); + bufClear(output); + memset(PtrUInt(input), 0, sizeof(TCommandBuffer)); + memcpy(PtrUInt(pchar('echo one two')), PtrUInt(input), 13); + params := getParams(input^); + Assert((paramCount(params) = 2) and stringEquals(getParam(1, params), 'two') and + (getParam(2, params) = nil), 'parser terminates at final token'); + findCommand('EcHo')^.method(params, nil, output, nil); + Assert(stringEquals(output^.buf, 'one two '#10), 'registered ECHO dispatch'); + freeParams(params); + memset(PtrUInt(input), ord('x'), sizeof(TCommandBuffer)); + params := getParams(input^); + Assert((params <> nil) and (stringSize(params^.param) = sizeof(TCommandBuffer)), + 'parser bounded without terminator'); + freeParams(params); + memset(PtrUInt(input), ord(' '), sizeof(TCommandBuffer)); + input^[1023] := ord('z'); + params := getParams(input^); + Assert((params <> nil) and stringEquals(params^.param, 'z'), 'parser final byte'); + freeParams(params); + input^[0] := 0; + params := getParams(input^); + Assert((paramCount(params) = 0) and (getParam(0, params) = nil), 'empty parameters'); + freeParams(params); + Assert(getParam(0, nil) = nil, 'nil parameter lookup'); + end else Assert(false, 'stdio test allocation'); + freeOutBuf(output); kfree(void(input)); + kfree(void(Commands)); + Commands := oldCommands; CmdCount := oldCount; CmdCap := oldCap; + Assert(lmm_total_free = beforeFree, 'stdio allocation ownership restored'); + arch.platform.diagnostics.testSummary('STDIO', 'STDIO_TESTS_PASSED', 'STDIO_TESTS_FAILED', passed, failed); end; { ---- Initialization ---- } procedure init; begin - { Allocate initial command array } - CmdCap := INITIAL_CMD_CAP; - CmdCount := 0; - Commands := PCommand(kalloc(CmdCap * SizeOf(TCommand))); - memset(uint32(Commands), 0, CmdCap * SizeOf(TCommand)); + if Commands <> nil then exit; + if not growCommands then exit; + UnitTest; Halted := false; HaltID := 0; HaltCB := nil; { Register built-in commands } - registerCommand('VERSION', @cmd_version, 'Display the running version of Asuro.'); - registerCommand('CLEAR', @cmd_clear, 'Clear the Screen.'); + registerCommand('CLEAR', @cmd_clear, 'Clear the Screen.'); registerCommand('HELP', @cmd_help, 'Lists all registered commands and their description.'); registerCommand('ECHO', @cmd_echo, 'Echo''s text to the terminal.'); - registerCommand('TIME', @cmd_time, 'Print the current time.'); - registerCommand('REBOOT', @cmd_reboot, 'Reboot the system.'); + end; - io.syslog.logln('STDIO', 'Initialized.'); -end; - -initialization - boot.mgr.registerBoot('io.stdio', @init, 'Standard I/O Abstraction', BOOT_MGR_BARRIER_EARLY); end. diff --git a/src/io/io.types.pas b/src/io/io.types.pas new file mode 100644 index 00000000..deb6a5a7 --- /dev/null +++ b/src/io/io.types.pas @@ -0,0 +1,53 @@ +// Copyright 2026 Aaron Hance +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +{ Shared I/O data contracts without console, driver or runtime dependencies. + Types extracted from io.stdio by Kieron Morris. } +unit io.types; +interface +type + { Output buffer — kalloc'd, growable } + POutBuf = ^TOutBuf; + TOutBuf = record + buf : pchar; + len : uint32; + cap : uint32; + end; + + { Parameter linked list } + PParamList = ^TParamList; + TParamList = record + Param : pchar; + Next : PParamList; + end; + + { Command buffer type } + TCommandBuffer = array[0..1023] of byte; + + { Command method — receives params plus stdin (read), stdout (write), stderr (write) } + TCommandMethod = procedure(params: PParamList; stdin_buf, stdout_buf, stderr_buf: POutBuf); + + { Command record } + PCommand = ^TCommand; + TCommand = record + registered : boolean; + hidden : boolean; + command : pchar; + method : TCommandMethod; + description : pchar; + end; + + { Halt callback } + THaltCallback = procedure(); + +implementation +end. diff --git a/src/memory/memory.bitmap.pas b/src/memory/memory.bitmap.pas new file mode 100644 index 00000000..657225d6 --- /dev/null +++ b/src/memory/memory.bitmap.pas @@ -0,0 +1,98 @@ +// Copyright 2021 Kieron Morris & Aaron Hance +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +{ Shared bitmap operations extracted from memory.heap. } +unit memory.bitmap; +interface +function find_free_run(bmp: PuInt32; start, count, total: uint32): sint32; +procedure bitmap_set_range(bmp: PuInt32; start, count: uint32); +procedure bitmap_clear_range(bmp: PuInt32; start, count: uint32); +function bitmap_all_set(bmp: PuInt32; start, count: uint32): boolean; +implementation +function find_free_run(bmp: PuInt32; start, count, total: uint32): sInt32; +var + idx, run_len, dw_idx: uint32; + dw: uint32; +begin + find_free_run := -1; + if (count = 0) or (count > total) then exit; + idx := start; + while idx <= total - count do begin + dw_idx := idx shr 5; + dw := bmp[dw_idx]; + + { Fast skip: entire dword fully allocated } + if dw = $FFFFFFFF then begin + idx := (dw_idx + 1) shl 5; + continue; + end; + + { Check if bit at idx is free (0) } + if (dw and (uint32(1) shl (idx and 31))) <> 0 then begin + inc(idx); + continue; + end; + + { Free bit at idx - verify contiguous run of 'count' } + run_len := 0; + while run_len < count do begin + if idx + run_len >= total then break; + if (bmp[(idx + run_len) shr 5] and + (uint32(1) shl ((idx + run_len) and 31))) <> 0 then + break; + inc(run_len); + end; + + if run_len >= count then begin + find_free_run := sInt32(idx); + exit; + end; + + { Skip past the used bit that broke the run } + idx := idx + run_len + 1; + end; +end; + +procedure bitmap_set_range(bmp: PuInt32; start, count: uint32); +var + i: uint32; +begin + if count = 0 then exit; + for i := start to start + count - 1 do + bmp[i shr 5] := bmp[i shr 5] or (uint32(1) shl (i and 31)); +end; + +procedure bitmap_clear_range(bmp: PuInt32; start, count: uint32); +var + i: uint32; +begin + if count = 0 then exit; + for i := start to start + count - 1 do + bmp[i shr 5] := bmp[i shr 5] and not (uint32(1) shl (i and 31)); +end; + +{ Check that all bits in a range are set (for double-free detection). } +function bitmap_all_set(bmp: PuInt32; start, count: uint32): boolean; +var + i: uint32; +begin + bitmap_all_set := true; + if count = 0 then exit; + for i := start to start + count - 1 do begin + if (bmp[i shr 5] and (uint32(1) shl (i and 31))) = 0 then begin + bitmap_all_set := false; + exit; + end; + end; +end; + +end. diff --git a/src/memory/memory.heap.pas b/src/memory/memory.heap.pas index 44987c91..fea5db6a 100644 --- a/src/memory/memory.heap.pas +++ b/src/memory/memory.heap.pas @@ -1,4 +1,5 @@ // Copyright 2021 Kieron Morris +// Copyright 2026 Aaron Hance // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -20,9 +21,9 @@ tracks which 8-byte allocation units are free or allocated. Layout of a 4 MiB heap page: - [0x000000..0x000017] THeapPageHeader (24 bytes) - [0x000018..0x00FC17] Bitmap (64512 bytes = 516096 bits) - [0x00FC18..0x00FFFF] Padding (1000 bytes) + Header: 24 bytes on i386, 32 bytes on x86_64 + Bitmap: 64512 bytes immediately following the header + Padding: remainder of the first 64 KiB [0x010000..0x3FFFFF] Data area (4128768 bytes = 516096 x 8) Each allocation prepends a 16-byte size prefix (4 bytes for the unit @@ -40,13 +41,7 @@ unit memory.heap; interface -uses - boot.mgr, - arch.x86.memory.physical, - io.syslog, - debug.tracer, - core.util, arch.x86.util, core.panic, - arch.x86.memory.virtual; +uses memory.bitmap; const ALLOC_UNIT = 8; { bytes per allocation unit } @@ -76,162 +71,52 @@ var Search_Page : PHeapPageHeader; procedure init; -function kalloc(size : uint32) : void; -function klalloc(size : uint32) : void; -procedure klfree(address : uint32); -function kpalloc(address : uint32) : void; +procedure UnitTest; +function kalloc(size : SizeUInt) : void; +function klalloc(size : SizeUInt) : void; +procedure klfree(address : PtrUInt); +function kpalloc(address : PtrUInt) : void; procedure kfree(area : void); { Statistics } -function lmm_total_free : uint32; +function lmm_total_free : SizeUInt; function lmm_page_count : uint32; implementation +uses arch.platform.memory, arch.platform.cpu, arch.platform.diagnostics; +procedure GPF; +begin fatal('LMM', 'FAIL: heap invariant'); end; +procedure heap_log(message: pchar); +begin arch.platform.diagnostics.log('LMM', message); end; +procedure heap_oom(message: pchar); +begin fatal('OOM', message); end; + { ==== Internal helpers ==== } function get_bitmap(hp: PHeapPageHeader): PuInt32; inline; begin - get_bitmap := PuInt32(uint32(hp) + uint32(sizeof(THeapPageHeader))); + get_bitmap := PuInt32(PtrUInt(hp) + uint32(sizeof(THeapPageHeader))); end; -function get_data_base(hp: PHeapPageHeader): uint32; inline; +function get_data_base(hp: PHeapPageHeader): PtrUInt; inline; begin - get_data_base := uint32(hp) + DATA_OFFSET; + get_data_base := PtrUInt(hp) + DATA_OFFSET; end; -{ Zero a memory region using rep stosd + rep stosb. } -procedure zero_mem(dest: Pointer; bytes: uint32); -var - d: Pointer; - b: uint32; -begin - d := dest; - b := bytes; - if b = 0 then exit; - asm - push edi - mov edi, d - mov ecx, b - shr ecx, 2 - xor eax, eax - cld - rep stosd - mov ecx, b - and ecx, 3 - rep stosb - pop edi - end; -end; - -{ Find a contiguous run of 'count' zero (free) bits in the bitmap - starting at bit index 'start'. Returns the starting index or -1. } -function find_free_run(bmp: PuInt32; start, count, total: uint32): sInt32; -var - idx, run_len, dw_idx: uint32; - dw: uint32; -begin - find_free_run := -1; - idx := start; - while idx + count <= total do begin - dw_idx := idx shr 5; - dw := bmp[dw_idx]; - - { Fast skip: entire dword fully allocated } - if dw = $FFFFFFFF then begin - idx := (dw_idx + 1) shl 5; - continue; - end; - - { Check if bit at idx is free (0) } - if (dw and (uint32(1) shl (idx and 31))) <> 0 then begin - inc(idx); - continue; - end; - - { Free bit at idx - verify contiguous run of 'count' } - run_len := 0; - while run_len < count do begin - if idx + run_len >= total then break; - if (bmp[(idx + run_len) shr 5] and - (uint32(1) shl ((idx + run_len) and 31))) <> 0 then - break; - inc(run_len); - end; - - if run_len >= count then begin - find_free_run := sInt32(idx); - exit; - end; - - { Skip past the used bit that broke the run } - idx := idx + run_len + 1; - end; -end; - -procedure bitmap_set_range(bmp: PuInt32; start, count: uint32); -var - i: uint32; -begin - for i := start to start + count - 1 do - bmp[i shr 5] := bmp[i shr 5] or (uint32(1) shl (i and 31)); -end; - -procedure bitmap_clear_range(bmp: PuInt32; start, count: uint32); -var - i: uint32; -begin - for i := start to start + count - 1 do - bmp[i shr 5] := bmp[i shr 5] and not (uint32(1) shl (i and 31)); -end; - -{ Check that all bits in a range are set (for double-free detection). } -function bitmap_all_set(bmp: PuInt32; start, count: uint32): boolean; -var - i: uint32; -begin - bitmap_all_set := true; - for i := start to start + count - 1 do begin - if (bmp[i shr 5] and (uint32(1) shl (i and 31))) = 0 then begin - bitmap_all_set := false; - exit; - end; - end; -end; - -{ Restore interrupt flag if it was enabled before cli. - Uses sti instead of popfd to avoid restoring dangerous EFLAGS bits - (IOPL, NT, VM, AC) that could cause arch.x86.fault.gpf or Bad-TSS on next IRET. } -procedure restore_if(saved: uint32); inline; -begin - if (saved and $200) <> 0 then - asm sti end; -end; +procedure zero_mem(dest: Pointer; bytes: SizeUInt); +begin arch.platform.cpu.zero(dest, bytes); end; +procedure restore_if(saved: PtrUInt); inline; +begin arch.platform.cpu.unlock(saved); end; { ==== Page management ==== } -function new_lmm_page: uint32; -var - i: uint32; -begin - push_trace('memory.heap.new_lmm_page'); - new_lmm_page := 0; - i := KERNEL_PAGE_NUMBER + 4; - while i < 1024 do begin - if arch.x86.memory.virtual.new_page(i) then begin - new_lmm_page := i shl 22; - pop_trace; - exit; - end; - inc(i); - end; - io.syslog.logln('LMM', 'ERROR: new_lmm_page failed.'); - pop_trace; -end; +function new_lmm_page: PtrUInt; +begin new_lmm_page := arch.platform.memory.allocateHeapPage; end; function new_heap_page(current: PHeapPageHeader): PHeapPageHeader; var - addr: uint32; + addr: PtrUInt; bmp: PuInt32; i: uint32; begin @@ -284,7 +169,7 @@ begin end; { Free the virtual page (returns physical block to PMM) } - arch.x86.memory.virtual.free_page_at_address(uint32(hp)); + arch.platform.memory.release(PtrUInt(hp), 1); pop_trace; end; @@ -294,143 +179,97 @@ end; procedure init; begin push_trace('memory.heap.init'); - io.syslog.logln('LMM', 'INIT BEGIN.'); + heap_log('INIT BEGIN.'); Root_Page := new_heap_page(nil); Search_Page := Root_Page; if Root_Page = nil then begin - io.syslog.logln('LMM', 'FATAL: Could not allocate root heap page!'); + heap_log('FATAL: Could not allocate root heap page!'); GPF; end; - io.syslog.logln('LMM', 'INIT END.'); + heap_log('INIT END.'); pop_trace; end; -function kpalloc(address: uint32): void; -var - block: uint16; -begin - push_trace('memory.heap.kpalloc'); - block := address shr 22; - if block >= 1024 then begin - io.syslog.logln('LMM', 'ERROR: kpalloc block out of range.'); - GPF; - kpalloc := nil; - pop_trace; - exit; - end; - io.syslog.log('LMM', 'kpalloc: identity-map block '); - io.syslog.writeintln(block); - force_alloc_block(block, 0); - map_page(block, block); - kpalloc := void(block shl 22); - pop_trace; -end; +function kpalloc(address: PtrUInt): void; +begin kpalloc := void(arch.platform.memory.identityReserve(address)); end; -procedure klfree(address: uint32); +procedure klfree(address: PtrUInt); var - page_num : uint16; num_pages : uint32; - i : uint32; begin push_trace('memory.heap.klfree'); num_pages := PuInt32(address)^; - if (num_pages = 0) or (num_pages > 256) then begin - io.syslog.logln('LMM', 'ERROR: klfree invalid page count.'); + if (num_pages = 0) or (num_pages > MAX_HEAP_REGIONS) then begin + heap_log('ERROR: klfree invalid page count.'); GPF; pop_trace; exit; end; - page_num := address shr 22; - for i := 0 to num_pages - 1 do - arch.x86.memory.virtual.free_page(page_num + uint16(i)); + arch.platform.memory.release(address, num_pages); pop_trace; end; -function klalloc(size: uint32): void; +function klalloc(size: SizeUInt): void; var - total_size : uint32; + total_size : SizeUInt; pages : uint32; - curr_page : uint16; - i : uint16; - miss : boolean; - base_addr : uint32; + base_addr : PtrUInt; begin push_trace('memory.heap.klalloc'); klalloc := void(nil); { Need extra 16 bytes for header: [page_count:4][magic:4][pad:8] } + if (size = 0) or (size > High(SizeUInt) - PAGE_SIZE_LMM - SIZE_PREFIX) then begin + pop_trace; exit; + end; total_size := size + 16; + if ((total_size + PAGE_SIZE_LMM - 1) div PAGE_SIZE_LMM) > MAX_HEAP_REGIONS then begin + pop_trace; exit; + end; pages := (total_size + PAGE_SIZE_LMM - 1) div PAGE_SIZE_LMM; if pages = 0 then pages := 1; - { Scan low virtual pages (below core.version space) for contiguous free range } - curr_page := 5; - while uint32(curr_page) + pages - 1 < KERNEL_PAGE_NUMBER do begin - miss := false; - for i := 0 to pages - 1 do begin - if not page_mappable(curr_page + i) then begin - miss := true; - curr_page := curr_page + i; - break; - end; - end; - if not miss then begin - for i := 0 to pages - 1 do - arch.x86.memory.virtual.new_page(curr_page + i); - base_addr := uint32(curr_page) shl 22; - zero_mem(Pointer(base_addr), 16); - { Header: [page_count:4][magic:4][pad:8] → user at +16 (16-aligned) } - PuInt32(base_addr)^ := pages; - PuInt32(base_addr + 4)^ := LARGE_ALLOC_MAGIC; - klalloc := void(base_addr + 16); - pop_trace; - exit; - end; - inc(curr_page); + base_addr := arch.platform.memory.allocateLarge(pages); + if base_addr <> 0 then begin + zero_mem(Pointer(base_addr), SIZE_PREFIX); + PuInt32(base_addr)^ := pages; + PuInt32(base_addr + 4)^ := LARGE_ALLOC_MAGIC; + klalloc := void(base_addr + SIZE_PREFIX); + pop_trace; exit; end; - io.syslog.logln('LMM', 'ERROR: klalloc no contiguous pages.'); + heap_log('ERROR: klalloc no contiguous pages.'); pop_trace; end; -function kalloc(size: uint32): void; [public, alias: 'kernel_kalloc']; +function kalloc(size: SizeUInt): void; [public, alias: 'kernel_kalloc']; var units_needed : uint32; hp : PHeapPageHeader; bmp : PuInt32; idx : sInt32; - alloc_base : uint32; - saved_flags : uint32; + alloc_base : PtrUInt; + saved_flags : PtrUInt; begin { Disable interrupts - save EFLAGS to local then cli. Uses pushfd;cli;pop (stack-balanced) instead of bare pushfd/popfd across the function, which is unsafe because FPC does not track stack pointer changes inside asm blocks. } - asm - pushfd - cli - pop saved_flags - end; + saved_flags := arch.platform.cpu.lock; kalloc := nil; - if size = 0 then begin + if (size = 0) or (size > High(SizeUInt) - PAGE_SIZE_LMM - SIZE_PREFIX) then begin restore_if(saved_flags); exit; end; - { Units needed: user size + 8-byte prefix, rounded up to ALLOC_UNIT } - units_needed := (size + SIZE_PREFIX + ALLOC_UNIT - 1) div ALLOC_UNIT; - {$IFDEF DEBUG_LMM} - inc(units_needed); { extra unit for guard sentinel } - {$ENDIF} - - { Large allocations that exceed a single heap page } - if units_needed > TOTAL_UNITS then begin + { Keep every allocation start 16-byte aligned, including odd sizes. } + if size > TOTAL_UNITS * ALLOC_UNIT - SIZE_PREFIX - 16 then begin kalloc := klalloc(size); if kalloc = nil then begin restore_if(saved_flags); - core.panic.panic('OOM', 'kalloc: large allocation failed', nil); + heap_oom('kalloc: large allocation failed'); exit; end; zero_mem(Pointer(kalloc), size); @@ -438,6 +277,8 @@ begin exit; end; + units_needed := ((size + SIZE_PREFIX + {$IFDEF DEBUG_LMM}8 + {$ENDIF}15) div 16) * 2; + hp := Search_Page; while true do begin { Only try pages with enough free units } @@ -485,7 +326,7 @@ begin hp := new_heap_page(hp); if hp = nil then begin restore_if(saved_flags); - core.panic.panic('OOM', 'kalloc: out of memory', nil); + heap_oom('kalloc: out of memory'); exit; end; end else begin @@ -502,43 +343,40 @@ var hp : PHeapPageHeader; bmp : PuInt32; prefix_val : uint32; - alloc_base : uint32; + alloc_base : PtrUInt; unit_idx : uint32; - base : uint32; - saved_flags : uint32; + base : PtrUInt; + saved_flags : PtrUInt; begin if area = nil then exit; { Disable interrupts - stack-balanced save to local variable } - asm - pushfd - cli - pop saved_flags - end; + saved_flags := arch.platform.cpu.lock; { Check for large allocation: magic sentinel at area - 12 (klalloc header is [page_count:4][MAGIC:4][pad:8][user_data]) } - if PuInt32(uint32(area) - 12)^ = LARGE_ALLOC_MAGIC then begin - base := uint32(area) - 16; + if PuInt32(PtrUInt(area) - 12)^ = LARGE_ALLOC_MAGIC then begin + base := PtrUInt(area) - 16; klfree(base); restore_if(saved_flags); exit; end; { Read the size prefix (SIZE_PREFIX bytes before the returned pointer) } - prefix_val := PuInt32(uint32(area) - SIZE_PREFIX)^; + prefix_val := PuInt32(PtrUInt(area) - SIZE_PREFIX)^; { Normal heap allocation } - hp := PHeapPageHeader((uint32(area) shr 22) shl 22); + hp := PHeapPageHeader((PtrUInt(area) shr 22) shl 22); bmp := get_bitmap(hp); { Compute allocation base and unit index } - alloc_base := uint32(area) - SIZE_PREFIX; + alloc_base := PtrUInt(area) - SIZE_PREFIX; unit_idx := (alloc_base - get_data_base(hp)) div ALLOC_UNIT; { Bounds check } - if (prefix_val = 0) or (unit_idx + prefix_val > hp^.TotalUnits) then begin - io.syslog.logln('LMM', 'ERROR: kfree invalid range.'); + if (prefix_val = 0) or (unit_idx >= hp^.TotalUnits) or + (prefix_val > hp^.TotalUnits - unit_idx) then begin + heap_log('ERROR: kfree invalid range.'); GPF; restore_if(saved_flags); exit; @@ -547,7 +385,7 @@ begin {$IFDEF DEBUG_LMM} { Guard byte overrun detection } if PuInt32(alloc_base + prefix_val * ALLOC_UNIT - 4)^ <> GUARD_MAGIC then begin - io.syslog.logln('LMM', 'ERROR: kfree guard corrupted - buffer overrun!'); + heap_log('ERROR: kfree guard corrupted - buffer overrun!'); GPF; restore_if(saved_flags); exit; @@ -556,7 +394,7 @@ begin { Double-free detection } if not bitmap_all_set(bmp, unit_idx, prefix_val) then begin - io.syslog.logln('LMM', 'ERROR: kfree double free!'); + heap_log('ERROR: kfree double free!'); GPF; restore_if(saved_flags); exit; @@ -578,7 +416,7 @@ end; { ==== Statistics ==== } -function lmm_total_free: uint32; +function lmm_total_free: SizeUInt; var hp: PHeapPageHeader; begin @@ -602,7 +440,44 @@ begin end; end; -initialization - boot.mgr.registerBoot('memory.heap', @init, 'Heap Memory Manager', 'arch.*.memory.*'); +procedure UnitTest; +var + a, b, large: void; + beforeFree: SizeUInt; + beforeFrames: uint64; + passed, failed: uint32; + procedure Assert(condition: boolean; name: pchar); + begin + if condition then begin inc(passed); heap_log(name); end + else begin inc(failed); heap_log('FAIL: heap test'); heap_log(name); end; + end; +begin + passed := 0; failed := 0; + beforeFree := lmm_total_free; + beforeFrames := arch.platform.memory.freeBacking; + a := kalloc(1); b := kalloc(17); + Assert((a <> nil) and (b <> nil) and ((PtrUInt(a) and 15) = 0) and + ((PtrUInt(b) and 15) = 0), 'PASS: heap odd sizes remain 16-byte aligned'); + Assert((PUInt8(a)^ = 0) and (PUInt8(PtrUInt(b) + 16)^ = 0), 'PASS: heap allocation zeroed'); + PUInt8(a)^ := 123; PUInt8(PtrUInt(b) + 16)^ := 45; + kfree(a); kfree(b); kfree(nil); + a := kalloc(1); + Assert(PUInt8(a)^ = 0, 'PASS: heap reused storage zeroed'); + kfree(a); + large := kalloc(PAGE_SIZE_LMM + 17); + Assert((large <> nil) and ((PtrUInt(large) and 15) = 0), 'PASS: heap large allocation aligned'); + PUInt8(large)^ := 87; + PUInt8(PtrUInt(large) + PAGE_SIZE_LMM + 16)^ := 91; + Assert((PUInt8(large)^ = 87) and (PUInt8(PtrUInt(large) + PAGE_SIZE_LMM + 16)^ = 91), + 'PASS: heap large allocation first/last bytes writable'); + kfree(large); + Assert((kalloc(0) = nil) and (kalloc(High(SizeUInt)) = nil), 'PASS: heap rejects zero/overflow sizes'); + Assert((lmm_total_free = beforeFree) and (lmm_page_count = 1), 'PASS: heap free accounting restored'); + Assert(arch.platform.memory.freeBacking = beforeFrames, 'PASS: heap backing frames restored'); + arch.platform.diagnostics.testSummary('LMM', 'HEAP_TESTS_PASSED', 'HEAP_TESTS_FAILED', passed, failed); + if failed <> 0 then GPF; +end; + + end. \ No newline at end of file diff --git a/src/memory/memory.raw.pas b/src/memory/memory.raw.pas new file mode 100644 index 00000000..7e564efd --- /dev/null +++ b/src/memory/memory.raw.pas @@ -0,0 +1,50 @@ +// Copyright 2026 Aaron Hance +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +{ Allocation-free scalar memory operations with native addresses. } +unit memory.raw; +interface +procedure fill(dest: pointer; value: uint8; bytes: SizeUInt); +procedure move(source, dest: pointer; bytes: SizeUInt); +implementation +type PNative = ^PtrUInt; +procedure fill(dest: pointer; value: uint8; bytes: SizeUInt); +var address, pattern: PtrUInt; +begin + address := PtrUInt(dest); + pattern := (High(PtrUInt) div 255) * value; + while bytes >= sizeof(PtrUInt) do begin + PNative(address)^ := pattern; + inc(address, sizeof(PtrUInt)); dec(bytes, sizeof(PtrUInt)); + end; + while bytes <> 0 do begin PUInt8(address)^ := value; inc(address); dec(bytes); end; +end; +procedure move(source, dest: pointer; bytes: SizeUInt); +var src, dst: PtrUInt; +begin + src := PtrUInt(source); dst := PtrUInt(dest); + if (src = dst) or (bytes = 0) then exit; + if (dst > src) and (dst - src < bytes) then begin + while bytes <> 0 do begin + dec(bytes); PUInt8(dst + bytes)^ := PUInt8(src + bytes)^; + end; + end else begin + while bytes >= sizeof(PtrUInt) do begin + PNative(dst)^ := PNative(src)^; + inc(dst, sizeof(PtrUInt)); inc(src, sizeof(PtrUInt)); dec(bytes, sizeof(PtrUInt)); + end; + while bytes <> 0 do begin + PUInt8(dst)^ := PUInt8(src)^; inc(dst); inc(src); dec(bytes); + end; + end; +end; +end. diff --git a/src/proc/proc.mgr.pas b/src/proc/proc.mgr.pas index ad933bd0..59bb2eca 100644 --- a/src/proc/proc.mgr.pas +++ b/src/proc/proc.mgr.pas @@ -1,4 +1,5 @@ // Copyright 2026 Kieron Morris +// Copyright 2026 Aaron Hance // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -27,18 +28,10 @@ unit proc.mgr; interface uses - boot.mgr, - arch.x86.bda, - driver.storage.fdtable, - core.ds.lists, - memory.heap, - proc.types, - core.rand, - io.stdio, - core.strings, - io.syslog, - debug.tracer, - core.util, arch.x86.util; + core.ds.dynamic, memory.heap, memory.raw, proc.types, proc.scheduler, + io.types, driver.storage.fdtable, arch.common.interrupts, arch.common.legacyirq, + arch.platform.process, arch.platform.diagnostics, arch.platform.cpu, + io.stdio, core.strings; { Core API } function create(name : pchar; @@ -46,6 +39,8 @@ function create(name : pchar; localData : void; priority : uint8) : PProcessContext; +function createSuspended(name: pchar; entry: TProcessEntryPoint; + localData: void; priority: uint8): PProcessContext; function findByID(pid : uint32) : PProcessContext; function findByName(name : pchar) : PProcessContext; procedure sendMessage(pid : uint32; msg : TProcessSysMsg; data : void); @@ -102,6 +97,67 @@ var implementation +type PProcessSlot = ^PtrUInt; + PProcessState = ^TProcessState; + +procedure log(message: pchar); +begin arch.platform.diagnostics.log('PROCMGR', message); end; +procedure releaseFileTable(ctx: PProcessContext); +begin + if ctx^.FDTable <> nil then fd_table_free(PFDTable(ctx^.FDTable)); + ctx^.FDTable := nil; +end; +function rootDirectory: pchar; +begin + rootDirectory := pchar(kalloc(2)); + if rootDirectory <> nil then begin rootDirectory[0] := '/'; rootDirectory[1] := #0; end; +end; +function nameEquals(left, right: pchar): boolean; +var i: uint32; +begin + nameEquals := false; + if (left = nil) or (right = nil) then exit; + i := 0; + while i < 32 do begin + if left[i] <> right[i] then exit; + if left[i] = #0 then begin nameEquals := true; exit; end; + inc(i); + end; +end; +var nextProcessID: uint32; + reaperProcess: PProcessContext; + reapPending: boolean; +procedure requestReap; +begin + { Called with IRQs excluded. Coalesces completed tasks into one worker wake. } + reapPending := true; + if (reaperProcess <> nil) and (reaperProcess^.State = psAwaiting) then + reaperProcess^.State := psReady; +end; +procedure reaperEntry(ctx: PProcessContext); +var flags: PtrUInt; +begin + while true do begin + flags := arch.common.interrupts.lock; + reapPending := false; + arch.common.interrupts.unlock(flags); + reapFinished; + flags := arch.common.interrupts.lock; + if not reapPending then ctx^.State := psAwaiting; + arch.common.interrupts.unlock(flags); + proc_await; + end; +end; +function newProcessID: uint32; +var flags: PtrUInt; +begin + flags := arch.common.interrupts.lock; + { IDs are never recycled; reject exhaustion rather than aliasing live IDs. } + newProcessID := nextProcessID; + if nextProcessID <> 0 then inc(nextProcessID); + arch.common.interrupts.unlock(flags); +end; + { ---- Internal helpers ---- } procedure copyName(dest : pchar; src : pchar; maxLen : uint32); @@ -127,27 +183,29 @@ function createInternal(name : pchar; priority : uint8; runnable : boolean) : PProcessContext; var ctx : PProcessContext; - slotPtr : ^uint32; + slotPtr : PProcessSlot; stack : void; - sp : uint32; prio : uint8; + savedFlags: PtrUInt; begin push_trace('proc.mgr.create'); createInternal := nil; + if (entry = nil) or (name = nil) or (Processes = nil) then begin pop_trace; exit; end; if priority = 0 then prio := 1 else prio := priority; { Allocate process context } ctx := PProcessContext(kalloc(SizeOf(TProcessContext))); if ctx = nil then begin - io.syslog.logln('PROCMGR', 'Failed to allocate process context.'); + log('Failed to allocate process context.'); pop_trace; exit; end; - memset(uint32(ctx), 0, SizeOf(TProcessContext)); + memory.raw.fill(pointer(ctx), 0, SizeOf(TProcessContext)); { Identity } - ctx^.ProcessID := rand32; + ctx^.ProcessID := newProcessID; + if ctx^.ProcessID = 0 then begin kfree(void(ctx)); pop_trace; exit; end; copyName(@ctx^.Name[0], name, 32); ctx^.ParentID := 0; if CurrentProcess <> nil then @@ -175,7 +233,7 @@ begin { Resources } ctx^.Resources := void(DL_New(SizeOf(TResourceBinding))); if ctx^.Resources = nil then begin - io.syslog.logln('PROCMGR', 'Failed to allocate resource list.'); + log('Failed to allocate resource list.'); kfree(void(ctx)); pop_trace; exit; @@ -184,7 +242,7 @@ begin { Per-process file descriptor table } ctx^.FDTable := void(fd_table_new); if ctx^.FDTable = nil then begin - io.syslog.logln('PROCMGR', 'Failed to allocate FD table.'); + log('Failed to allocate FD table.'); DL_Free(PDList(ctx^.Resources)); kfree(void(ctx)); pop_trace; @@ -192,10 +250,10 @@ begin end; { Per-process working directory } - ctx^.Cwd := stringCopy('/'); + ctx^.Cwd := rootDirectory; if ctx^.Cwd = nil then begin - io.syslog.logln('PROCMGR', 'Failed to allocate working directory.'); - fd_table_free(PFDTable(ctx^.FDTable)); + log('Failed to allocate working directory.'); + releaseFileTable(ctx); DL_Free(PDList(ctx^.Resources)); kfree(void(ctx)); pop_trace; @@ -205,89 +263,46 @@ begin { Allocate per-process core.version stack } stack := kalloc(PROCESS_STACK_SIZE); if stack = nil then begin - io.syslog.logln('PROCMGR', 'Failed to allocate process stack.'); + log('Failed to allocate process stack.'); kfree(void(ctx^.Cwd)); - fd_table_free(PFDTable(ctx^.FDTable)); + releaseFileTable(ctx); DL_Free(PDList(ctx^.Resources)); kfree(void(ctx)); pop_trace; exit; end; ctx^.StackBase := stack; - ctx^.StackTop := uint32(stack) + PROCESS_STACK_SIZE; + ctx^.StackTop := PtrUInt(stack) + PROCESS_STACK_SIZE; - { Build fake interrupt frame on the stack so the first context switch - IRET's into process_trampoline. - - Stack layout (growing downward): - [StackTop - 4] EFLAGS ($0202 = IF set) - [StackTop - 8] CS ($08 = core.version code) - [StackTop - 12] EIP (-> process_trampoline) - [StackTop - 16] EAX (0) - [StackTop - 20] ECX (0) - [StackTop - 24] EDX (0) - [StackTop - 28] EBX (0) - [StackTop - 32] ESP (ignored by POPAD) - [StackTop - 36] EBP (0) - [StackTop - 40] ESI (0) - [StackTop - 44] EDI (0) - [StackTop - 48] DS ($10 = core.version data, padded to 32-bit) - [StackTop - 52] ES ($10) - [StackTop - 56] FS ($10) - [StackTop - 60] GS ($10) - SavedESP = StackTop - 60 - } - sp := ctx^.StackTop; - - { IRET frame } - sp := sp - 4; PuInt32(sp)^ := $0202; { EFLAGS: IF=1 } - sp := sp - 4; PuInt32(sp)^ := $08; { CS } - sp := sp - 4; PuInt32(sp)^ := uint32(@process_trampoline); { EIP } - - { POPAD frame (8 registers) } - sp := sp - 4; PuInt32(sp)^ := 0; { EAX } - sp := sp - 4; PuInt32(sp)^ := 0; { ECX } - sp := sp - 4; PuInt32(sp)^ := 0; { EDX } - sp := sp - 4; PuInt32(sp)^ := 0; { EBX } - sp := sp - 4; PuInt32(sp)^ := 0; { ESP (ignored by POPAD) } - sp := sp - 4; PuInt32(sp)^ := 0; { EBP } - sp := sp - 4; PuInt32(sp)^ := 0; { ESI } - sp := sp - 4; PuInt32(sp)^ := 0; { EDI } - - { Segment registers (pushed as 32-bit values) } - sp := sp - 4; PuInt32(sp)^ := $10; { DS } - sp := sp - 4; PuInt32(sp)^ := $10; { ES } - sp := sp - 4; PuInt32(sp)^ := $10; { FS } - sp := sp - 4; PuInt32(sp)^ := $10; { GS } - - ctx^.SavedESP := sp; + ctx^.SavedStack := arch.platform.process.prepare(pointer(stack), PROCESS_STACK_SIZE, + @process_trampoline); { Add to process table } - asm pushf; cli end; - slotPtr := DL_Add(Processes); + savedFlags := arch.common.interrupts.lock; + slotPtr := PProcessSlot(DL_Add(Processes)); if slotPtr = nil then begin - asm popf end; - io.syslog.logln('PROCMGR', 'Failed to allocate process table slot.'); + arch.common.interrupts.unlock(savedFlags); + log('Failed to allocate process table slot.'); if ctx^.StackBase <> nil then kfree(ctx^.StackBase); if ctx^.Cwd <> nil then kfree(void(ctx^.Cwd)); - if ctx^.FDTable <> nil then - fd_table_free(PFDTable(ctx^.FDTable)); + if ctx^.FDTable <> nil then begin + releaseFileTable(ctx); + end; if ctx^.Resources <> nil then DL_Free(PDList(ctx^.Resources)); kfree(void(ctx)); pop_trace; exit; end; - slotPtr^ := uint32(ctx); + slotPtr^ := PtrUInt(ctx); { Mark as ready for scheduling } if runnable then ctx^.State := psReady; - asm popf end; + arch.common.interrupts.unlock(savedFlags); - io.syslog.log('PROCMGR', 'Created process: '); - io.syslog.writestringln(name); + log(name); pop_trace; createInternal := ctx; @@ -301,6 +316,12 @@ begin create := createInternal(name, entry, localData, priority, true); end; +function createSuspended(name: pchar; entry: TProcessEntryPoint; + localData: void; priority: uint8): PProcessContext; +begin + createSuspended := createInternal(name, entry, localData, priority, false); +end; + { ---- Process trampoline ---- } { This is the EIP that IRET jumps to on the first schedule of a process. It calls the process entry point, and when it returns, marks the process @@ -315,9 +336,7 @@ begin end; { Wait for the scheduler to reap us } while true do begin - asm - hlt - end; + arch.platform.cpu.idle; end; end; @@ -326,13 +345,13 @@ end; function findByID(pid : uint32) : PProcessContext; var i : uint32; - slotPtr : ^uint32; + slotPtr : PProcessSlot; ctx : PProcessContext; begin findByID := nil; - if Processes = nil then exit; + if DL_Size(Processes) = 0 then exit; for i := 0 to DL_Size(Processes) - 1 do begin - slotPtr := DL_Get(Processes, i); + slotPtr := PProcessSlot(DL_Get(Processes, i)); if slotPtr <> nil then begin ctx := PProcessContext(slotPtr^); if ctx^.ProcessID = pid then begin @@ -346,16 +365,16 @@ end; function findByName(name : pchar) : PProcessContext; var i : uint32; - slotPtr : ^uint32; + slotPtr : PProcessSlot; ctx : PProcessContext; begin findByName := nil; - if Processes = nil then exit; + if DL_Size(Processes) = 0 then exit; for i := 0 to DL_Size(Processes) - 1 do begin - slotPtr := DL_Get(Processes, i); + slotPtr := PProcessSlot(DL_Get(Processes, i)); if slotPtr <> nil then begin ctx := PProcessContext(slotPtr^); - if stringEquals(@ctx^.Name[0], name) then begin + if nameEquals(@ctx^.Name[0], name) then begin findByName := ctx; exit; end; @@ -365,74 +384,69 @@ end; { ---- Messaging ---- } -procedure sendMessage(pid : uint32; msg : TProcessSysMsg; data : void); -var - ctx : PProcessContext; +procedure sendMessage(pid: uint32; msg: TProcessSysMsg; data: void); +var ctx: PProcessContext; savedFlags: PtrUInt; begin + savedFlags := arch.common.interrupts.lock; ctx := findByID(pid); if ctx <> nil then begin - ctx^.PendingMsg := msg; ctx^.MsgData := data; - { If the process is awaiting, wake it up on any message } - if ctx^.State = psAwaiting then - ctx^.State := psReady; - { If suspended and receiving smResume, wake it } - if (ctx^.State = psSuspended) and (msg = smResume) then - ctx^.State := psReady; + ctx^.PendingMsg := msg; + if (ctx^.State = psAwaiting) or + ((ctx^.State = psSuspended) and (msg = smResume)) then ctx^.State := psReady; end; + arch.common.interrupts.unlock(savedFlags); end; procedure terminate(pid : uint32); begin if pid = 0 then exit; { Cannot terminate idle } + if (reaperProcess <> nil) and (pid = reaperProcess^.ProcessID) then exit; sendMessage(pid, smTerminate, nil); end; -procedure kill(pid : uint32); -var - ctx : PProcessContext; +procedure kill(pid: uint32); +var ctx: PProcessContext; savedFlags: PtrUInt; begin - if pid = 0 then exit; { Cannot kill idle } + if pid = 0 then exit; + savedFlags := arch.common.interrupts.lock; ctx := findByID(pid); - if ctx <> nil then begin - asm pushf; cli end; - ctx^.State := psFinished; - ctx^.ExitCode := $FFFFFFFF; - asm popf end; + if (ctx <> nil) and (ctx <> reaperProcess) then begin + ctx^.State := psFinished; ctx^.ExitCode := High(uint32); requestReap; end; + arch.common.interrupts.unlock(savedFlags); end; { ---- Process self-management ---- } procedure proc_yield; +var savedFlags: PtrUInt; begin if CurrentProcess <> nil then begin - asm pushf; cli end; + savedFlags := arch.common.interrupts.lock; CurrentProcess^.TicksUsed := CurrentProcess^.Quantum; - asm popf end; + arch.common.interrupts.unlock(savedFlags); { Next timer tick will preempt us } - asm - hlt - end; + arch.platform.cpu.idle; end; end; -procedure proc_sleep_ms(ms : uint32); -var - ticks : uint32; - start, now, elapsed : uint32; +procedure proc_sleep_ms(ms: uint32); +var remaining: uint64; + previous, now, delta: uint32; begin - ticks := (ms * 1024) div 1000; - if ticks = 0 then ticks := 1; - start := Counters.c32; + remaining := (uint64(ms) * PIT_INPUT_HZ + uint64(PIT_DIVISOR) * 1000 - 1) div (PIT_DIVISOR * 1000); + if remaining = 0 then remaining := 1; + previous := arch.platform.process.timerTicks; repeat proc_yield; - now := Counters.c32; - if now >= start then - elapsed := now - start - else - elapsed := ($FFFFFFFF - start) + now + 1; - until elapsed >= ticks; + now := arch.platform.process.timerTicks; + if now >= previous then delta := now - previous + else delta := (High(uint32) - previous) + now + 1; + previous := now; + if remaining <= delta then exit; + remaining := remaining - delta; + until false; end; procedure proc_await; @@ -441,20 +455,19 @@ begin This procedure just spin-waits until an ISR sets the state back to something other than psAwaiting. } if CurrentProcess <> nil then begin - while CurrentProcess^.State = psAwaiting do - asm hlt end; + while PProcessState(@CurrentProcess^.State)^ = psAwaiting do + arch.platform.cpu.idle; end; end; procedure proc_suspend; +var savedFlags: PtrUInt; begin if CurrentProcess <> nil then begin - asm pushf; cli end; + savedFlags := arch.common.interrupts.lock; CurrentProcess^.State := psSuspended; - asm popf end; - asm - hlt - end; + arch.common.interrupts.unlock(savedFlags); + arch.platform.cpu.idle; end; end; @@ -464,65 +477,58 @@ begin CurrentProcess^.ExitCode := exitCode; CurrentProcess^.State := psFinished; while true do begin - asm - hlt - end; + arch.platform.cpu.idle; end; end; end; { ---- Resource binding ---- } -procedure bindResource(ctx : PProcessContext; kind : TResourceKind; handle : void; cleanup : TResourceCleanup); -var - resList : PDList; - binding : PResourceBinding; +procedure bindResource(ctx: PProcessContext; kind: TResourceKind; handle: void; cleanup: TResourceCleanup); +var binding: PResourceBinding; savedFlags: PtrUInt; begin if ctx = nil then exit; - resList := PDList(ctx^.Resources); - if resList = nil then exit; - binding := PResourceBinding(DL_Add(resList)); - binding^.Kind := kind; - binding^.Handle := handle; - binding^.Cleanup := cleanup; -end; - -procedure unbindResource(ctx : PProcessContext; handle : void); -var - resList : PDList; - binding : PResourceBinding; - i : uint32; -begin - if ctx = nil then exit; - resList := PDList(ctx^.Resources); - if resList = nil then exit; - for i := 0 to DL_Size(resList) - 1 do begin - binding := PResourceBinding(DL_Get(resList, i)); - if binding^.Handle = handle then begin - if binding^.Cleanup <> nil then - binding^.Cleanup(binding^.Handle); - DL_Delete(resList, i); - exit; + savedFlags := arch.common.interrupts.lock; + if ctx^.Resources <> nil then begin + binding := PResourceBinding(DL_Add(PDList(ctx^.Resources))); + if binding <> nil then begin + binding^.Kind := kind; binding^.Handle := handle; binding^.Cleanup := cleanup; end; end; + arch.common.interrupts.unlock(savedFlags); end; -procedure unbindResourceNoCleanup(ctx : PProcessContext; handle : void); -var - resList : PDList; - binding : PResourceBinding; - i : uint32; +procedure unbindResource(ctx: PProcessContext; handle: void); +var resList: PDList; binding: PResourceBinding; i: uint32; + cleanup: TResourceCleanup; savedFlags: PtrUInt; begin if ctx = nil then exit; + savedFlags := arch.common.interrupts.lock; resList := PDList(ctx^.Resources); - if resList = nil then exit; - for i := 0 to DL_Size(resList) - 1 do begin + cleanup := nil; + if DL_Size(resList) > 0 then for i := 0 to DL_Size(resList) - 1 do begin binding := PResourceBinding(DL_Get(resList, i)); if binding^.Handle = handle then begin + cleanup := binding^.Cleanup; DL_Delete(resList, i); - exit; + break; end; end; + arch.common.interrupts.unlock(savedFlags); + if assigned(cleanup) then cleanup(handle); +end; + +procedure unbindResourceNoCleanup(ctx: PProcessContext; handle: void); +var resList: PDList; binding: PResourceBinding; i: uint32; savedFlags: PtrUInt; +begin + if ctx = nil then exit; + savedFlags := arch.common.interrupts.lock; + resList := PDList(ctx^.Resources); + if DL_Size(resList) > 0 then for i := 0 to DL_Size(resList) - 1 do begin + binding := PResourceBinding(DL_Get(resList, i)); + if binding^.Handle = handle then begin DL_Delete(resList, i); break; end; + end; + arch.common.interrupts.unlock(savedFlags); end; procedure unbindAllResources(ctx : PProcessContext); @@ -530,12 +536,15 @@ var resList : PDList; binding : PResourceBinding; i : sint32; + savedFlags: PtrUInt; begin if ctx = nil then exit; + savedFlags := arch.common.interrupts.lock; resList := PDList(ctx^.Resources); - if resList = nil then exit; + if resList = nil then begin arch.common.interrupts.unlock(savedFlags); exit; end; { Detach early so cleanup callbacks that re-enter find nothing } ctx^.Resources := nil; + arch.common.interrupts.unlock(savedFlags); for i := sint32(DL_Size(resList)) - 1 downto 0 do begin binding := PResourceBinding(DL_Get(resList, uint32(i))); if binding <> nil then begin @@ -564,6 +573,7 @@ function runCommand(name : pchar; method : TCommandMethod; params : PParamList; stdin_buf, stdout_buf, stderr_buf : POutBuf) : PProcessContext; var ctx : PProcessContext; + savedFlags: PtrUInt; begin ctx := createInternal(name, @legacy_entry, void(method), 1, false); if ctx <> nil then begin @@ -573,9 +583,9 @@ begin ctx^.StdErr := stderr_buf; { Publish only after all fields read by legacy_entry are initialized. psCreated is also runnable, so creation must use psSuspended. } - asm pushf; cli end; + savedFlags := arch.common.interrupts.lock; ctx^.State := psReady; - asm popf end; + arch.common.interrupts.unlock(savedFlags); end; runCommand := ctx; end; @@ -592,12 +602,12 @@ end; function getProcessByIndex(idx : uint32) : PProcessContext; var - slotPtr : ^uint32; + slotPtr : PProcessSlot; begin getProcessByIndex := nil; if Processes = nil then exit; if idx >= DL_Size(Processes) then exit; - slotPtr := DL_Get(Processes, idx); + slotPtr := PProcessSlot(DL_Get(Processes, idx)); if slotPtr <> nil then getProcessByIndex := PProcessContext(slotPtr^); end; @@ -606,101 +616,44 @@ end; procedure reapFinished; var - i : sint32; - slotPtr : ^uint32; - ctx : PProcessContext; - parent : PProcessContext; + i: sint32; + ctx: PProcessContext; + savedFlags: PtrUInt; begin - if Processes = nil then exit; - { Iterate in reverse so index shifting from DL_Delete is safe } - for i := sint32(DL_Size(Processes)) - 1 downto 0 do begin - slotPtr := DL_Get(Processes, uint32(i)); - if slotPtr = nil then continue; - ctx := PProcessContext(slotPtr^); - if (ctx^.State = psFinished) or (ctx^.State = psError) then begin - { Don't reap the currently running process } - if ctx = CurrentProcess then continue; - { Never reap the idle process } - if ctx^.ProcessID = 0 then continue; - { Notify parent (if it exists) that a child has exited } - if ctx^.ParentID <> 0 then begin - parent := findByID(ctx^.ParentID); - if parent <> nil then - sendMessage(parent^.ProcessID, smChildExited, void(ctx^.ProcessID)); + { The reaper calls this in process context, never from the timer ISR. + Detach under IRQ exclusion before callbacks can re-enter the registry. } + i := sint32(DL_Size(Processes)) - 1; + while i >= 0 do begin + savedFlags := arch.common.interrupts.lock; + ctx := getProcessByIndex(uint32(i)); + if (ctx <> nil) and (ctx <> CurrentProcess) and (ctx^.ProcessID <> 0) and + ((ctx^.State = psFinished) or (ctx^.State = psError)) then + if not fd_table_begin_close(PFDTable(ctx^.FDTable)) then begin + ctx := nil; { final FD completion wakes the reaper } end; - { Clean up file descriptors } - if ctx^.FDTable <> nil then begin - fd_table_free(PFDTable(ctx^.FDTable)); - ctx^.FDTable := nil; - end; - { Free per-process CWD } - if ctx^.Cwd <> nil then begin - kfree(void(ctx^.Cwd)); - ctx^.Cwd := nil; - end; - { Clean up resources } - unbindAllResources(ctx); - { Free stack } - if ctx^.StackBase <> nil then - kfree(ctx^.StackBase); - { Remove from table } + if (ctx <> nil) and (ctx <> CurrentProcess) and (ctx^.ProcessID <> 0) and + ((ctx^.State = psFinished) or (ctx^.State = psError)) then begin DL_Delete(Processes, uint32(i)); - { Free context } + arch.common.interrupts.unlock(savedFlags); + releaseFileTable(ctx); + if ctx^.Cwd <> nil then kfree(void(ctx^.Cwd)); + unbindAllResources(ctx); + if ctx^.StackBase <> nil then kfree(ctx^.StackBase); + if ctx^.ParentID <> 0 then + sendMessage(ctx^.ParentID, smChildExited, void(PtrUInt(ctx^.ProcessID))); kfree(void(ctx)); - end; + end else arch.common.interrupts.unlock(savedFlags); + dec(i); end; end; { ---- Scheduler ---- } function scheduler_pick_next : PProcessContext; -var - count : uint32; - i : uint32; - idx : uint32; - slotPtr : ^uint32; - ctx : PProcessContext; begin - { Reap dead processes first } - reapFinished; - - count := DL_Size(Processes); - - { Check if current process still has quantum left } - if CurrentProcess^.State = psRunning then begin - CurrentProcess^.TicksUsed := CurrentProcess^.TicksUsed + 1; - if CurrentProcess^.TicksUsed < CurrentProcess^.Quantum then begin - scheduler_pick_next := CurrentProcess; - exit; - end; - { Quantum expired, move to ready } - CurrentProcess^.State := psReady; - CurrentProcess^.TicksUsed := 0; - end; - - { Round-robin: scan from SchedulerIndex, skipping idle } - if SchedulerIndex >= count then - SchedulerIndex := 0; - - for i := 0 to count - 1 do begin - idx := (SchedulerIndex + i) mod count; - slotPtr := DL_Get(Processes, idx); - if slotPtr = nil then continue; - ctx := PProcessContext(slotPtr^); - if ctx^.ProcessID = 0 then continue; { Skip idle in round-robin } - if (ctx^.State = psReady) or (ctx^.State = psCreated) then begin - ctx^.State := psRunning; - ctx^.TicksUsed := 0; - SchedulerIndex := (idx + 1) mod count; - scheduler_pick_next := ctx; - exit; - end; - end; - - { No runnable process found — fall back to idle } - IdleProcess^.State := psRunning; - IdleProcess^.TicksUsed := 0; - scheduler_pick_next := IdleProcess; + if (CurrentProcess^.State = psFinished) or (CurrentProcess^.State = psError) then requestReap; + scheduler_pick_next := proc.scheduler.pickNext(CurrentProcess, IdleProcess, + DL_Size(Processes), SchedulerIndex, @getProcessByIndex); end; { ---- Terminal commands ---- } @@ -720,7 +673,7 @@ end; procedure terminal_command_ps(params : PParamList; stdin_buf, stdout_buf, stderr_buf : POutBuf); var i : uint32; - slotPtr : ^uint32; + slotPtr : PProcessSlot; ctx : PProcessContext; stStr : pchar; begin @@ -729,9 +682,9 @@ begin bufWriteStrLn(stdout_buf, 'PID NAME STATE PRI'); bufWriteStrLn(stdout_buf, '---------- ------------------------- ----------- ---'); - if Processes <> nil then begin + if (Processes <> nil) and (DL_Size(Processes) > 0) then begin for i := 0 to DL_Size(Processes) - 1 do begin - slotPtr := DL_Get(Processes, i); + slotPtr := PProcessSlot(DL_Get(Processes, i)); if slotPtr = nil then continue; ctx := PProcessContext(slotPtr^); @@ -820,22 +773,25 @@ begin pop_trace; end; + { ---- Init ---- } procedure init; var - slotPtr : ^uint32; + slotPtr : PProcessSlot; begin push_trace('proc.mgr.init'); - io.syslog.logln('PROCMGR', 'INIT BEGIN.'); + log('INIT BEGIN.'); + nextProcessID := 1; + fd_set_idle_hook(@requestReap); { Create process table } - Processes := DL_New(SizeOf(uint32)); + Processes := DL_New(SizeOf(PtrUInt)); SchedulerIndex := 0; { Create idle process — PID 0, unkillable, uses core.version main stack } IdleProcess := PProcessContext(kalloc(SizeOf(TProcessContext))); - memset(uint32(IdleProcess), 0, SizeOf(TProcessContext)); + memory.raw.fill(pointer(IdleProcess), 0, SizeOf(TProcessContext)); IdleProcess^.ProcessID := 0; copyName(@IdleProcess^.Name[0], 'Idle', 32); IdleProcess^.State := psRunning; @@ -844,19 +800,21 @@ begin IdleProcess^.PendingMsg := smNone; IdleProcess^.Resources := void(DL_New(SizeOf(TResourceBinding))); IdleProcess^.FDTable := void(fd_table_new); - IdleProcess^.Cwd := stringCopy('/'); + IdleProcess^.Cwd := rootDirectory; { No stack allocation — idle runs on the core.version's original stack } - slotPtr := DL_Add(Processes); - slotPtr^ := uint32(IdleProcess); + slotPtr := PProcessSlot(DL_Add(Processes)); + slotPtr^ := PtrUInt(IdleProcess); CurrentProcess := IdleProcess; + reapPending := false; + reaperProcess := createInternal('reaper', @reaperEntry, nil, 1, false); + if reaperProcess <> nil then reaperProcess^.State := psAwaiting; - io.syslog.logln('PROCMGR', 'Idle process created (PID 0).'); + log('Idle process created (PID 0).'); - io.syslog.logln('PROCMGR', 'INIT END.'); + log('INIT END.'); pop_trace; end; -initialization - boot.mgr.registerBoot('proc.mgr', @Init, 'Process Manager', 'driver.storage.fs*'); + end. diff --git a/src/proc/proc.scheduler.pas b/src/proc/proc.scheduler.pas new file mode 100644 index 00000000..deb1241a --- /dev/null +++ b/src/proc/proc.scheduler.pas @@ -0,0 +1,67 @@ +// Copyright 2026 Aaron Hance +// Copyright 2026 Kieron Morris +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +{ Shared round-robin/quantum policy extracted from proc.mgr (Kieron Morris). + Caller excludes interrupts and owns process lifetime/reaping. } +unit proc.scheduler; +interface +uses proc.types; +type TGetProcess = function(index: uint32): PProcessContext; +function pickNext(current, idle: PProcessContext; count: uint32; + var cursor: uint32; getProcess: TGetProcess): PProcessContext; +implementation +function pickNext(current, idle: PProcessContext; count: uint32; + var cursor: uint32; getProcess: TGetProcess): PProcessContext; +var + i : uint32; + idx : uint32; + ctx : PProcessContext; +begin + { Check if current process still has quantum left } + if (current <> nil) and (current^.State = psRunning) then begin + current^.TicksUsed := current^.TicksUsed + 1; + if current^.TicksUsed < current^.Quantum then begin + pickNext := current; + exit; + end; + { Quantum expired, move to ready } + current^.State := psReady; + current^.TicksUsed := 0; + end; + + { Round-robin: scan from cursor, skipping idle } + if cursor >= count then + cursor := 0; + + if count > 0 then for i := 0 to count - 1 do begin + idx := (cursor + i) mod count; + ctx := getProcess(idx); + if ctx = nil then continue; + if ctx^.ProcessID = 0 then continue; { Skip idle in round-robin } + if (ctx^.State = psReady) or (ctx^.State = psCreated) then begin + ctx^.State := psRunning; + ctx^.TicksUsed := 0; + cursor := (idx + 1) mod count; + pickNext := ctx; + exit; + end; + end; + + { No runnable process found — fall back to idle } + if idle = nil then begin pickNext := nil; exit; end; + idle^.State := psRunning; + idle^.TicksUsed := 0; + pickNext := idle; +end; + +end. diff --git a/src/proc/proc.types.pas b/src/proc/proc.types.pas index dc4848ea..0691a825 100644 --- a/src/proc/proc.types.pas +++ b/src/proc/proc.types.pas @@ -1,3 +1,15 @@ +// Copyright 2026 Aaron Hance +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + { ProcTypes - Type definitions for the preemptive process management system. @@ -12,7 +24,7 @@ unit proc.types; interface uses - io.stdio; + io.types; const { 8 KB per-process core.version stack } @@ -90,9 +102,9 @@ type EntryPoint : TProcessEntryPoint; { Context switch state } - SavedESP : uint32; + SavedStack : PtrUInt; StackBase : void; - StackTop : uint32; + StackTop : PtrUInt; { Scheduling } Priority : uint8; diff --git a/toolchain/check_arch_boundaries.py b/toolchain/check_arch_boundaries.py new file mode 100644 index 00000000..c5a749a5 --- /dev/null +++ b/toolchain/check_arch_boundaries.py @@ -0,0 +1,43 @@ +#!/usr/bin/env python3 +# Copyright 2026 Aaron Hance +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# http://www.apache.org/licenses/LICENSE-2.0 +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Prevent shared Pascal units from selecting architecture implementations.""" +from pathlib import Path +import re +import sys + +cpu_directive = re.compile(r"\{\$\s*if(?:def|ndef)?\b[^}]*\bCPU\w*", re.I) +arch_import = re.compile(r"\barch\.(?:x86|x86_64)\.", re.I) +errors = [] +for path in sorted(Path("src").rglob("*")): + if path.suffix not in (".pas", ".inc"): + continue + if path.parts[1] == "arch" and path.parts[2] != "common": + continue + source = path.read_text(encoding="utf-8") + # Ignore prose comments but retain compiler directives. + source = re.sub(r"\{(?!\$).*?\}|\(\*.*?\*\)|//[^\n]*", "", source, flags=re.S) + if cpu_directive.search(source): + errors.append(f"{path}: CPU conditional belongs in build selection") + # Existing i386-only services outside the port still import their drivers. + # The migrated units must depend on stable platform names instead. + migrated = path.as_posix() in { + "src/core/core.util.pas", "src/core/strings/core.strings.pas", + "src/io/io.stdio.pas", "src/proc/proc.mgr.pas", + "src/memory/memory.heap.pas", "src/memory/memory.raw.pas", + } or path.parts[1:3] == ("arch", "common") + if migrated and arch_import.search(source): + errors.append(f"{path}: shared unit imports an architecture implementation") +if errors: + print("\n".join(errors), file=sys.stderr) + sys.exit(1) +print("Shared architecture boundaries verified.") diff --git a/toolchain/compile.sh b/toolchain/compile.sh index 485e29a6..ac81b0fa 100644 --- a/toolchain/compile.sh +++ b/toolchain/compile.sh @@ -1,4 +1,5 @@ #!/usr/bin/env bash +# Multi-architecture build updates: Copyright 2026 Aaron Hance. ERRCOUNT=0 echo " " echo "=======================" @@ -6,8 +7,11 @@ echo " " echo "Asuro Compilation" echo " " -#Compile Stub.asm -rm -f lib/* +TOOLCHAIN_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +source "$TOOLCHAIN_DIR/target.sh" +mkdir -p "$OBJ_DIR" "$BIN_DIR" +# Clean only this target's generated objects; preserve other architectures. +find "$OBJ_DIR" -maxdepth 1 -type f \( -name '*.o' -o -name '*.ppu' -o -name '*.a' \) -delete runOrFail() { local binary=$1 @@ -20,8 +24,6 @@ runOrFail() { fi } -TOOLCHAIN_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" - declare -a run_steps=( "compile_stub.sh" "Failed to compile stub!" "compile_vergen.sh" "Versions failed to compile" @@ -35,6 +37,16 @@ declare -a run_steps=( "compile_docs.sh" "Failed to generate documentation!" ) +if [ "$ASURO_ARCH" = x86_64 ]; then + # Bootstrap milestone: no desktop/runtime dependencies until they are ported. + run_steps=( + "compile_stub.sh" "Failed to compile stub!" + "compile_sources.sh" "Failed to compile FPC Sources!" + "compile_link.sh" "Failed linking!" + "compile_isogen.sh" "Failed to create ISO!" + ) +fi + for ((i=0; i<${#run_steps[@]}; i+=2)) do if [ "$ERRCOUNT" -eq "0" ] @@ -46,6 +58,11 @@ do done #Call generate final artifacts based on failure or success of the above. +if [ "$ASURO_ARCH" = x86_64 ]; then + if [ "$ERRCOUNT" -ne 0 ]; then exit 1; fi + echo "No errors. Bootstrap image: $ISO_FILE" + exit 0 +fi if [ "$ERRCOUNT" -ne "0" ] then . "${TOOLCHAIN_DIR}/compile_finish.sh" "failed" @@ -53,4 +70,4 @@ else . "${TOOLCHAIN_DIR}/compile_finish.sh" "success" fi -cd .. \ No newline at end of file +cd .. diff --git a/toolchain/compile_isogen.sh b/toolchain/compile_isogen.sh index 4af8522b..b39b9be8 100644 --- a/toolchain/compile_isogen.sh +++ b/toolchain/compile_isogen.sh @@ -1,8 +1,15 @@ #!/usr/bin/env bash +# Multi-architecture build updates: Copyright 2026 Aaron Hance. +set -e +source "$(dirname "${BASH_SOURCE[0]}")/target.sh" echo " " echo "=======================" echo " " echo "Creating ISO..." echo " " -cp bin/kernel.bin iso/boot/asuro.bin -grub-mkrescue -o Asuro.iso iso \ No newline at end of file +mkdir -p "$ISO_ROOT/boot/grub" +if [ "$ASURO_ARCH" != x86 ]; then + cp iso/boot/grub/grub.cfg "$ISO_ROOT/boot/grub/grub.cfg" +fi +cp "$BIN_DIR/kernel.bin" "$ISO_ROOT/boot/asuro.bin" +grub-mkrescue -o "$ISO_FILE" "$ISO_ROOT" diff --git a/toolchain/compile_link.sh b/toolchain/compile_link.sh index f72b2798..0140d802 100644 --- a/toolchain/compile_link.sh +++ b/toolchain/compile_link.sh @@ -1,11 +1,20 @@ #!/usr/bin/env bash +# Multi-architecture build updates: Copyright 2026 Aaron Hance. +set -e +source "$(dirname "${BASH_SOURCE[0]}")/target.sh" echo " " echo "=======================" echo " " echo "Linking..." echo " " +if [ "$ASURO_ARCH" = x86_64 ]; then + ld -m "$LD_FORMAT" --gc-sections --no-warn-execstack \ + -T"$LINKER_SCRIPT" -o "$BIN_DIR/kernel.bin" "$OBJ_DIR/"*.o + grub-file --is-x86-multiboot "$BIN_DIR/kernel.bin" + exit +fi objstring=""; -for object in `find lib/ -name "*.o"`; do +for object in `find lib/ -maxdepth 1 -name "*.o"`; do if [ "$object" != "lib/stub.o" ] then objstring=$objstring$object" "; @@ -19,4 +28,4 @@ echo " " LIBGCC=$(gcc -m32 -print-libgcc-file-name) echo "libgcc: ${LIBGCC}" -ld -m elf_i386 -s --gc-sections --no-warn-execstack -Ttoolchain/linker.script -o bin/kernel.bin $objstring --start-group lib/liblvgl.a ${LIBGCC} --end-group \ No newline at end of file +ld -m elf_i386 -s --gc-sections --no-warn-execstack -Ttoolchain/linker.script -o bin/kernel.bin $objstring --start-group lib/liblvgl.a ${LIBGCC} --end-group diff --git a/toolchain/compile_sources.sh b/toolchain/compile_sources.sh index b6b86a34..c4ce1d16 100644 --- a/toolchain/compile_sources.sh +++ b/toolchain/compile_sources.sh @@ -1,13 +1,33 @@ #!/usr/bin/env bash +# Multi-architecture build updates: Copyright 2026 Aaron Hance. +set -e +source "$(dirname "${BASH_SOURCE[0]}")/target.sh" echo " " echo "=======================" echo " " echo "Compiling FPC Sources..." echo " " -# Build -Fu flags for all directories under src/, wasuro/ and compat/ -FU_PATHS="-Fusrc -Fuwasuro -Fucompat" -for dir in $(find src wasuro compat -type d 2>/dev/null); do +# The build selects one platform implementation; shared units cannot select CPUs. +python3 toolchain/check_arch_boundaries.py +# Build -Fu flags for the selected architecture and shared sources. +if [ "$ASURO_ARCH" = x86_64 ]; then + test_flags=() + case "${ASURO_BOOT_TEST:-none}" in + none) ;; + ud) test_flags=(-dASURO_TEST_UD) ;; + pf) test_flags=(-dASURO_TEST_PF) ;; + df) test_flags=(-dASURO_TEST_DF) ;; + *) echo "Unknown ASURO_BOOT_TEST" >&2; exit 1 ;; + esac + # Restrict search paths: never accidentally select the i386 system unit. + exec ppcx64 -n -O2 -Si -Sc -Sg -Xd -CX -Rintel -Tlinux \ + -Fu"src/arch/$ASURO_ARCH/platform" -Fusrc/core -Fusrc/core/strings -Fusrc/driver/storage -Fusrc/arch/x86_64/proc -Fusrc/core/ds -Fusrc/proc -Fusrc/io -Fusrc/arch/common -Fusrc/debug -Fusrc/boot -Fusrc/memory -Fusrc/arch/x86_64/memory -Fu"src/arch/x86_64" -FU"$OBJ_DIR" -FE"$BIN_DIR" "${test_flags[@]}" \ + src/arch/x86_64/arch.x86_64.boot.pas +fi +FU_PATHS="-Fusrc/arch/$ASURO_ARCH/platform -Fusrc -Fuwasuro -Fucompat" +for dir in $(find src wasuro compat -type d -not -path 'src/arch/*' 2>/dev/null) \ + $(find "src/arch/$ASURO_ARCH" src/arch/common -type d -not -path '*/platform*'); do FU_PATHS="$FU_PATHS -Fu$dir" done -fpc -Aelf -gw -g -gl -n -v0e -O3 -OpPENTIUM3 -Si -Sc -Sg -Xd -CX -XXs -CfSSE -CfSSE2 -Rintel -Pi386 -Tlinux -FElib/ $FU_PATHS src/asuro.pas \ No newline at end of file +fpc -Aelf -gw -g -gl -n -v0e -O3 -OpPENTIUM3 -Si -Sc -Sg -Xd -CX -XXs -CfSSE -CfSSE2 -Rintel -Pi386 -Tlinux -FElib/ $FU_PATHS src/asuro.pas diff --git a/toolchain/compile_stub.sh b/toolchain/compile_stub.sh index efb7d588..1a506431 100644 --- a/toolchain/compile_stub.sh +++ b/toolchain/compile_stub.sh @@ -1,8 +1,15 @@ #!/usr/bin/env bash +# Multi-architecture build updates: Copyright 2026 Aaron Hance. +set -e +source "$(dirname "${BASH_SOURCE[0]}")/target.sh" echo " " echo "=======================" echo " " echo "Compiling Stub..." echo " " -nasm -f elf src/arch/x86/boot/stub.asm -o lib/stub.o -nasm -f elf src/boot/splash_tga.asm -o lib/splash_tga.o \ No newline at end of file +nasm -f "$NASM_FORMAT" "src/arch/$ASURO_ARCH/boot/stub.asm" -o "$OBJ_DIR/stub.o" +if [ "$ASURO_ARCH" = x86 ]; then + nasm -f "$NASM_FORMAT" src/boot/splash_tga.asm -o "$OBJ_DIR/splash_tga.o" +else + nasm -f "$NASM_FORMAT" src/arch/x86_64/isr/entry.asm -o "$OBJ_DIR/isr_entry.o" +fi diff --git a/toolchain/target.sh b/toolchain/target.sh new file mode 100644 index 00000000..9f4a2f54 --- /dev/null +++ b/toolchain/target.sh @@ -0,0 +1,36 @@ +#!/usr/bin/env bash +# Copyright 2026 Aaron Hance +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# http://www.apache.org/licenses/LICENSE-2.0 +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# Shared target selection. Source this file from build steps. +ASURO_ARCH="${ASURO_ARCH:-x86}" +case "$ASURO_ARCH" in + x86) + OBJ_DIR=lib + BIN_DIR=bin + ISO_ROOT=iso + ISO_FILE=Asuro.iso + NASM_FORMAT=elf + LD_FORMAT=elf_i386 + LINKER_SCRIPT=toolchain/linker.script + ;; + x86_64) + OBJ_DIR=lib/x86_64 + BIN_DIR=bin/x86_64 + ISO_ROOT=bin/x86_64/iso + ISO_FILE=Asuro-x86_64.iso + NASM_FORMAT=elf64 + LD_FORMAT=elf_x86_64 + LINKER_SCRIPT=toolchain/x86_64/linker.script + ;; + *) echo "Unsupported ASURO_ARCH: $ASURO_ARCH" >&2; exit 1 ;; +esac +export ASURO_ARCH OBJ_DIR BIN_DIR ISO_ROOT ISO_FILE NASM_FORMAT LD_FORMAT LINKER_SCRIPT diff --git a/toolchain/x86_64/linker.script b/toolchain/x86_64/linker.script new file mode 100644 index 00000000..664ee988 --- /dev/null +++ b/toolchain/x86_64/linker.script @@ -0,0 +1,31 @@ +/* Copyright 2026 Aaron Hance + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +OUTPUT_FORMAT(elf64-x86-64) +ENTRY(loader) +SECTIONS +{ + /* Bootstrap-only low mapping. Final kernel address-space policy is a + separate milestone; do not inherit the i386 0xC0000000 offset. */ + . = 0x100000; + kernel_start = .; + .multiboot : { KEEP(*(.multiboot)) } + .text ALIGN(4096) : { *(.text .text.*) } + .rodata ALIGN(4096) : { *(.rodata .rodata.*) } + .data ALIGN(4096) : { *(.data .data.*) } + load_end = .; + .bss ALIGN(4096) (NOLOAD) : { *(COMMON) *(.bss .bss.*) } + . = ALIGN(4096); + kernel_end = .; + ASSERT(kernel_end < 0x100000000, "Bootstrap must fit below 4 GiB") + /DISCARD/ : { *(.comment) *(.note*) *(.eh_frame*) } +} diff --git a/toolchain/x86_64/test_boot.ps1 b/toolchain/x86_64/test_boot.ps1 new file mode 100644 index 00000000..8bb650d8 --- /dev/null +++ b/toolchain/x86_64/test_boot.ps1 @@ -0,0 +1,112 @@ +# Copyright 2026 Aaron Hance +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# http://www.apache.org/licenses/LICENSE-2.0 +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Run from the workspace root after building the selected ASURO_BOOT_TEST. +# Restore temporary optical medium and optional RAM changes after the test. +param( + [ValidateSet('none', 'ud', 'pf', 'df')][string]$Test = 'none', + [ValidateRange(0, 8192)][int]$MemoryMB = 0, + [string]$Controller = 'AHCI', + [int]$Port = 1, + [int]$Device = 0, + [ValidateRange(5, 45)][int]$TimeoutSeconds = 35 +) +$ErrorActionPreference = 'Stop' +$config = Get-Content -LiteralPath .\localenv.json -Raw | ConvertFrom-Json +$vm = $config.virtualbox.MachineName +$serialPath = $config.virtualbox.LogLocation +if (-not $serialPath) { throw 'VirtualBox serial LogLocation is required.' } +$isoPath = (Resolve-Path -LiteralPath .\Asuro-x86_64.iso).Path +$state = & VBoxManage.exe showvminfo $vm --machinereadable +if ($LASTEXITCODE -ne 0) { throw 'Cannot inspect VirtualBox VM.' } +if ($state -notcontains 'VMState="poweroff"') { + throw 'The configured VM must already be powered off before boot validation.' +} +$mediumKey = '"{0}-{1}-{2}"=' -f $Controller, $Port, $Device +$mediumLine = @($state | Where-Object { $_.StartsWith($mediumKey) }) +if ($mediumLine.Count -ne 1) { throw 'Cannot identify original optical medium.' } +$originalMedium = $mediumLine[0].Substring($mediumKey.Length).Trim('"').Replace('\\', '\') +if ($originalMedium -notin @('none', 'emptydrive') -and $originalMedium -notmatch '(?i)\.iso$') { + throw 'Selected port is not an ISO/empty optical drive; refusing to replace it.' +} +if ($originalMedium -eq 'none') { $originalMedium = 'emptydrive' } +$evidence = Join-Path (Get-Location) 'bin\x86_64' +New-Item -ItemType Directory -Path $evidence -Force | Out-Null +$originalMemory = [int](($state | Where-Object { $_ -match '^memory=' }) -replace '^memory=', '') +if ($originalMemory -le 0) { throw 'Cannot identify original VM RAM size.' } +$started = $false +try { + if ($MemoryMB -ne 0) { + & VBoxManage.exe modifyvm $vm --memory $MemoryMB + if ($LASTEXITCODE -ne 0) { throw 'Cannot set test RAM size.' } + } + & VBoxManage.exe storageattach $vm --storagectl $Controller --port $Port --device $Device --type dvddrive --medium $isoPath + if ($LASTEXITCODE -ne 0) { throw 'Cannot attach bootstrap ISO.' } + & .\virtualbox-wrapper.ps1 -command up -Headless -NoMonitor + if ($LASTEXITCODE -ne 0) { throw 'Cannot start test VM.' } + $started = $true + $deadline = [DateTime]::UtcNow.AddSeconds($TimeoutSeconds) + $output = '' + $marker = if ($Test -eq 'none') { 'bootstrap complete; interrupts disabled' } else { 'HALTED: exception diagnostics complete' } + do { + if (Test-Path -LiteralPath $serialPath) { + $output = Get-Content -LiteralPath $serialPath -Raw + } + if ($output -and ($output.Contains($marker) -or $output.Contains('FAIL:'))) { break } + Start-Sleep -Milliseconds 200 + } while ([DateTime]::UtcNow -lt $deadline) + $output | Set-Content -LiteralPath (Join-Path $evidence "boot-$Test-$MemoryMB.log") + & VBoxManage.exe debugvm $vm getregisters rip rsp cs cr0 cr2 cr3 cr4 efer tr | + Set-Content -LiteralPath (Join-Path $evidence "registers-$Test-$MemoryMB.log") + if ($LASTEXITCODE -ne 0) { throw 'Cannot capture CPU registers.' } + Write-Output $output + if (-not $output -or -not $output.Contains($marker) -or $output.Contains('FAIL:')) { + throw 'Boot did not reach its expected completion marker.' + } + foreach ($required in @('EXCEPTION_TESTS_PASSED=0000000000000002', 'EXCEPTION_TESTS_FAILED=0000000000000000', + 'PMM_TESTS_FAILED=0000000000000000', 'VMM_TESTS_FAILED=0000000000000000', + 'HEAP_TESTS_FAILED=0000000000000000', 'IRQ_TESTS_FAILED=0000000000000000', 'CONTEXT_TESTS_FAILED=0000000000000000', 'PROCESS_TESTS_FAILED=0000000000000000', 'FD_TESTS_FAILED=0000000000000000', 'STRINGS_TESTS_FAILED=0000000000000000', 'STDIO_TESTS_FAILED=0000000000000000')) { + if (-not $output.Contains($required)) { throw "Missing check: $required" } + } + if ($MemoryMB -gt 4096 -and $output.Contains('SKIP: no available physical RAM above 4 GiB')) { + throw 'High physical memory test was skipped.' + } + if ($Test -ne 'none') { + $vector = @{ud = '6'; pf = 'E'; df = '8'}[$Test] + $errorCode = if ($Test -eq 'pf') { '2' } else { '0' } + foreach ($required in @("VECTOR=000000000000000$vector", "ERROR=000000000000000$errorCode", 'CS=0000000000000018')) { + if (-not $output.Contains($required)) { throw "Missing diagnostic: $required" } + } + if ($Test -eq 'pf' -and -not $output.Contains('CR2=0000000100000000')) { + throw 'Page fault did not preserve the expected CR2.' + } + if ($Test -eq 'df' -and (-not $output.Contains('STACK=double-fault IST1') -or + -not $output.Contains('RSP=0000000100001000'))) { + throw 'Double fault did not switch from the broken stack to IST1.' + } + } + Write-Output "PASS: VirtualBox x86_64 boot test '$Test'" +} finally { + try { + if ($started) { + & .\virtualbox-wrapper.ps1 -command down + if ($LASTEXITCODE -ne 0) { throw 'Cannot stop test VM.' } + } + } finally { + & VBoxManage.exe storageattach $vm --storagectl $Controller --port $Port --device $Device --type dvddrive --medium $originalMedium + if ($LASTEXITCODE -ne 0) { throw 'Cannot restore original optical medium.' } + if ($MemoryMB -ne 0) { + & VBoxManage.exe modifyvm $vm --memory $originalMemory + if ($LASTEXITCODE -ne 0) { throw 'Cannot restore original VM RAM size.' } + } + } +} diff --git a/virtualbox-wrapper.ps1 b/virtualbox-wrapper.ps1 index 1eb2c0a5..0768f3a7 100644 --- a/virtualbox-wrapper.ps1 +++ b/virtualbox-wrapper.ps1 @@ -10,7 +10,10 @@ You need a local git-ignored localenv.json file with the following content: param ( [Parameter(Mandatory=$true)] [ValidateSet('up', 'down')] - [String]$Command + [String]$Command, + # Optional modes for bounded, script-driven boot validation (Aaron Hance). + [switch]$Headless, + [switch]$NoMonitor ) $Config = Get-Content .\localenv.json | ConvertFrom-Json @@ -24,10 +27,23 @@ if ($Command -eq 'up') { Clear-Content $LogLocation } - $MonitorJob = Start-Job -ArgumentList $MachineName -ScriptBlock { - param($MachineName) + if ($NoMonitor) { + if ($Headless) { + VBoxManage.exe startvm $MachineName --type headless + } else { + VBoxManage.exe startvm $MachineName + } + exit $LASTEXITCODE + } + + $MonitorJob = Start-Job -ArgumentList $MachineName, $Headless.IsPresent -ScriptBlock { + param($MachineName, $Headless) Write-Output "Starting $MachineName" - VBoxManage.exe startvm $MachineName + if ($Headless) { + VBoxManage.exe startvm $MachineName --type headless + } else { + VBoxManage.exe startvm $MachineName + } $running=$true while($running) { $status=(VBoxManage.exe list runningvms) @@ -56,4 +72,4 @@ if ($Command -eq 'up') { } elseif ($Command -eq 'down') { Write-Output "Stopping $MachineName" VBoxManage.exe controlvm $MachineName poweroff -} \ No newline at end of file +}