diff --git a/.gitattributes b/.gitattributes index 23f5ec8..25af962 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,3 +1,25 @@ +# Git LFS for large media files *.mkv filter=lfs diff=lfs merge=lfs -text *.mp4 filter=lfs diff=lfs merge=lfs -text *.gif filter=lfs diff=lfs merge=lfs -text +*.png filter=lfs diff=lfs merge=lfs -text + +# Line endings +* text=auto eol=lf +*.bat text eol=crlf + +# Binary files +*.elf binary +*.uf2 binary +*.bin binary + +# Custom diff drivers +*.c diff=c +*.cpp diff=cpp +*.h diff=c +*.hpp diff=cpp +*.S diff=asm + +# Linker scripts +*.x text +*.ld text diff --git a/.gitignore b/.gitignore index 6467923..386ddbe 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,44 @@ +# Build directories /Build/ +/build/ +/build-*/ + +# IDE and editor files +.vscode/* +!.vscode/c_cpp_properties.json +!.vscode/settings.json +!.vscode/launch.json + +.idea/ +*.swp +*.swo +*~ +.DS_Store + +# Compiled binaries +*.o +*.elf +*.bin +*.uf2 +*.hex +*.dis +*.map + +# CMake artifacts +CMakeCache.txt +CMakeFiles/ +cmake_install.cmake +compile_commands.json +Makefile + +# Python +__pycache__/ +*.pyc +.env + +# Temporary files +*.tmp +*.log +cache/ +*.cache +.cache/ diff --git a/.vscode/c_cpp_properties.json b/.vscode/c_cpp_properties.json index 88e0567..dc8b7fe 100644 --- a/.vscode/c_cpp_properties.json +++ b/.vscode/c_cpp_properties.json @@ -1,16 +1,29 @@ { "configurations": [ { - "name": "Linux", + "name": "Pico-OS Kernel", "includePath": [ - "${workspaceFolder}/**" + "${workspaceFolder}", + "${workspaceFolder}/build/_deps/pico_sdk-src/src/common/pico_stdlib_headers/include", + "${workspaceFolder}/build/_deps/pico_sdk-src/src/rp2_common/hardware_base/include", + "${workspaceFolder}/build/_deps/pico_sdk-src/src/rp2040/hardware_structs/include", + "${workspaceFolder}/build/_deps/pico_sdk-src/src/rp2_common/hardware_claim/include", + "${workspaceFolder}/build/_deps/pico_sdk-src/src/rp2_common/hardware_sync/include", + "${workspaceFolder}/build/_deps/pico_sdk-src/src/rp2_common/hardware_sync_spin_lock/include", + "${workspaceFolder}/build/_deps/pico_sdk-src/src/rp2_common/hardware_irq/include", + "${workspaceFolder}/build/_deps/pico_sdk-src/src/rp2_common/pico_platform/include", + "${workspaceFolder}/build/generated/pico_base" ], - "defines": [], - "compilerPath": "/usr/bin/clang", + "defines": [ + "KERNEL", + "PICO_BOARD=\"pico\"", + "PICO_RP2040=1", + "PICO_32BIT=1" + ], + "compilerPath": "/usr/bin/clang++", "cStandard": "c11", "cppStandard": "c++20", - "intelliSenseMode": "clang-arm", - "compileCommands": "${workspaceFolder}/Build/compile_commands.json" + "intelliSenseMode": "macos-clang-arm64" } ], "version": 4 diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..1c5d61c --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,19 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "name": "Cortex Debug: Attach", + "type": "cortex-debug", + "request": "attach", + "servertype": "openocd", + "executable": "${workspaceFolder}/build/Kernel.elf", + "configFiles": [ + "interface/picoprobe.cfg", + "target/rp2040.cfg" + ], + "svdFile": "${workspaceFolder}/build/_deps/pico_sdk-src/src/rp2040/hardware_regs/rp2040.svd", + "runToEntryPoint": "main", + "cwd": "${workspaceFolder}" + } + ] +} diff --git a/CHANGELOG.md b/CHANGELOG.md index 61d2ad2..1a33e00 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,85 @@ +# Changelog + +## [Unreleased] - 2025-12-14 + +### Added +- **Final Polish Items**: + - `SoftwareSpinLock`: Active locking with deadlock detection and power efficiency (`wfe`/`sev`). + - `SoftwareMutex`: Passive locking using `HardwareSpinLock`. + - `WaitingThreadQueue`: For managing threads waiting on resources. +- **Hardware Integration**: + - `HardwareSpinLock`: Memory-mapped spin lock support for RP2040. + - `sys$read`, `sys$write`: Basic system calls hooked to `ConsoleDevice`. + - **MPU**: Supervisor/User memory separation verified. +- **Core Infrastructure**: + - `init_array`/`fini_array` support in crt0 for C++ static constructors. + - Stack alignment fixes to prevent hardfaults. + - **Exception Handling**: Implemented HardFault handler (Userland kill, Kernel panic). + - **Memory**: Added `allocate_eternal` for silent allocation. + - **QEMU**: Added launch script `Tools/run-qemu.sh`. + +### Refactoring & Core Improvements +- **Kernel/StackProtector**: Implemented `init_stack_guard` using RP2040 ROSC for hardware-randomized stack canaries. +- **Kernel/Exceptions**: Enhanced HardFault handlers with clearer register dumps and stack trace headers. +- **Kernel/PageAllocator**: Added detailed comments on RP2040 RAM layout and Buddy System initialization. +- **Kernel/GlobalMemoryAllocator**: Standardized mutex usage with `ScopedMutex` and added comments. Implemented dynamic heap growth. +- **Kernel/StackWrapper**: Improved API docs and parameter naming. +- **Tools**: Removed unstable QEMU/Renode scripts (emulation deferred). + +### System +- **Synchronization**: `HardwareSpinLock` is now ISR-safe, storing/restoring interrupt state. +- **MPU**: Setup for supervisor/user mode isolation. +- **Build**: Linker script (`linker.ld`) enforces 8-byte alignment for stack/heap. +- **Documentation**: + - Added `docs/InterruptSafety.md`. +- **Multi-Core Support**: `Scheduler` now supports creating and scheduling threads on both cores of the RP2040. Integrated `HardwareSpinLock` for safe inter-core synchronization. +- **Robust Data Structures**: + - Replaced `Std::SortedSet`'s binary search tree with a **Red-Black Tree** implementation, guaranteeing O(log n) operations. + - Added `m_allocated_pages` to `PageAllocator` using `SortedSet` to explicitly track allocated memory ranges. +- **Userland Allocator**: Implemented a free-list based `malloc`/`free`/`realloc`/`calloc` in `Userland/LibC`, replacing the previous bump allocator. This allows memory reuse and reduces fragmentation. +- `Std::Formatter` specialization for `Kernel::PageRange`. + +### Changed +- Refactored `ConsoleFileHandle` to store a reference to the specific `VirtualFile` instance, fixing an issue where `stat /dev/tty` returned incorrect device information. +- Updated `Scheduler::dump()` to correctly iterate over core-specific thread arrays. +- Enhanced `PageAllocator::dump()` to show detailed allocation tracking. + +### Fixed +- `stat /dev/tty` listing incorrect file attributes by decoupling `ConsoleFileHandle` from the singleton. +- Compilation errors in `Std/SortedSet.hpp` related to `HashTable` compatibility (relied on strict weak ordering `<` instead of `==`). + +## [Unreleased] - 2025-12-12 + +### Fixed +- **macOS Build Compatibility**: Added `Tools/compat/elf.h` with minimal ELF definitions for macOS. +- **Portable File I/O**: Replaced Linux-specific `memfd_create`/`copy_file_range` with ANSI C alternatives (`tmpfile`, `fread`/`fwrite`). +- **Missing Includes**: Added `MaskedInterruptGuard.hpp` includes to `Process.cpp`, `SystemHandler.cpp`, `Loader.cpp`, `Scheduler.cpp`, and `KernelMutex.hpp`. +- **Stack Alignment**: Fixed potential hardfaults by ensuring 8-byte stack alignment in `Thread::setup_context()` and `hand_over_to_loaded_executable()`. +- **strlcpy Conflict**: Updated `Std/Forward.hpp` to use standard `size_t` return type. +- **Linker Symbol Resolution**: Hardcoded RP2040 RAM boundaries in `PageAllocator.cpp` to bypass missing linker script definitions. +- **Iterator Const Correctness**: Fixed `SortedSet::InorderIterator` comparison operators to accept const references. +- **Test Suite**: Fixed narrowing conversion, undeclared identifiers, and string arithmetic warnings in test files. + +### Added +- **C++ Static Constructors**: Implemented `preinit_array`, `init_array`, and `fini_array` calling in `Userland/LibC/sys/crt0.c`. +- **Non-Executable Stack**: Added `.note.GNU-stack` section to `crt0.S` to silence linker warnings. + +### Changed +- Refactored `HardwareSpinLock` to correct lock acquisition logic (removed infinite loop on success) and added `wfe`/`sev` instructions for power-efficient SMP synchronization. +- Enhanced `SoftwareSpinLock` with **Deadlock Detection** (checks if core already holds the lock) and `wfe`/`sev` support. +- Updated `.gitignore` to allow useful VSCode configuration files. +- Removed `-lbsd` dependency from `Tools/CMakeLists.txt`. +- Removed unsupported `-fropi`/`-frwpi` compiler flags from `Userland/CMakeLists.txt`. +- Commented out custom `operator new`/`delete` in `GlobalMemoryAllocator.cpp` to avoid conflicts with Pico SDK. + +### Added +- **VSCode Configuration**: Added `c_cpp_properties.json` for IntelliSense and `launch.json` for Cortex-Debug support. +- **C++ Static Constructors**: Implemented `preinit_array`, `init_array`, and `fini_array` calling in `Userland/LibC/sys/crt0.c`. + +--- + +## Previous Changes + ### Major - Added `AbstractLock` which is used by the new `LockGuard`. diff --git a/CMakeLists.txt b/CMakeLists.txt index 56779bd..4bfdb52 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,40 +1,142 @@ +# ============================================================================== +# PicoOS - CMake Build Configuration +# ============================================================================== +# +# Build targets: +# - Kernel.1.elf : Main kernel binary with debug symbols +# - Kernel.1.uf2 : UF2 firmware for direct flashing +# - Kernel.elf : Stripped kernel for production +# +# Usage: +# mkdir build && cd build +# cmake .. -G Ninja +# ninja +# +# ============================================================================== + cmake_minimum_required(VERSION 3.19.5) +# ------------------------------------------------------------------------------ +# Build Configuration +# ------------------------------------------------------------------------------ + +# Export compile_commands.json for IDE support set(CMAKE_EXPORT_COMPILE_COMMANDS ON) -if (NOT CMAKE_BUILD_TYPE) - set(CMAKE_BUILD_TYPE Debug) +# Default to Debug build +if(NOT CMAKE_BUILD_TYPE) + set(CMAKE_BUILD_TYPE Debug CACHE STRING "Build type" FORCE) endif() +message(STATUS "Build type: ${CMAKE_BUILD_TYPE}") + +# ------------------------------------------------------------------------------ +# Pico SDK Setup +# ------------------------------------------------------------------------------ + set(CMAKE_MODULE_PATH ${CMAKE_SOURCE_DIR}/CMake) find_package(Tools MODULE) +# Import Pico SDK (will download if PICO_SDK_PATH not set) include(CMake/pico_sdk_import.cmake) -project(Pico C CXX ASM) +# ------------------------------------------------------------------------------ +# Project Definition +# ------------------------------------------------------------------------------ + +project(PicoOS + VERSION 0.4.0 + DESCRIPTION "A microkernel-inspired OS for the Raspberry Pi Pico" + LANGUAGES C CXX ASM +) + set(CMAKE_C_STANDARD 11) +set(CMAKE_C_STANDARD_REQUIRED ON) set(CMAKE_CXX_STANDARD 20) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +# Initialize Pico SDK pico_sdk_init() -# FIXME: This is mostly necessary for the SDK -set(DISABLE_WARNINGS -Wno-unused-parameter -Wno-type-limits) -set(DISABLE_CXX_WARNINGS -Wno-reorder -Wno-ignored-qualifiers) +# ------------------------------------------------------------------------------ +# Compiler Options +# ------------------------------------------------------------------------------ + +# Warnings to suppress from Pico SDK headers (not our code) +set(SDK_WARNINGS_OFF + -Wno-unused-parameter + -Wno-type-limits +) + +set(SDK_CXX_WARNINGS_OFF + -Wno-reorder + -Wno-ignored-qualifiers +) +# Common compile options interface add_library(project_options INTERFACE) -target_compile_options(project_options INTERFACE -fdiagnostics-color=always -Wall ${DISABLE_WARNINGS} -Wextra -Werror -O0) -target_compile_options(project_options INTERFACE $<$:-frtti ${DISABLE_CXX_WARNINGS}>) -target_include_directories(project_options INTERFACE ${CMAKE_SOURCE_DIR}) + +target_compile_options(project_options INTERFACE + -fdiagnostics-color=always + -Wall + -Wextra + -Werror + -O0 + -fstack-protector-strong + ${SDK_WARNINGS_OFF} +) + +target_compile_options(project_options INTERFACE + $<$: + -frtti + ${SDK_CXX_WARNINGS_OFF} + > +) + +target_include_directories(project_options INTERFACE + ${CMAKE_SOURCE_DIR} +) + +# ------------------------------------------------------------------------------ +# Userland Applications +# ------------------------------------------------------------------------------ add_subdirectory(Userland) -file(GLOB_RECURSE Kernel_SOURCES CONFIGURE_DEPENDS Kernel/*.cpp Kernel/*.S Std/*.cpp) +# ------------------------------------------------------------------------------ +# Kernel Build +# ------------------------------------------------------------------------------ + +file(GLOB_RECURSE KERNEL_SOURCES CONFIGURE_DEPENDS + Kernel/*.cpp + Kernel/*.S + Std/*.cpp +) + +add_executable(Kernel.1 ${KERNEL_SOURCES}) + +target_link_libraries(Kernel.1 + pico_stdlib + pico_bootrom + hardware_dma + project_options + LibEmbeddedFiles +) -add_executable(Kernel.1 ${Kernel_SOURCES}) -target_link_libraries(Kernel.1 pico_stdlib pico_bootrom hardware_dma project_options LibEmbeddedFiles) -target_compile_definitions(Kernel.1 PRIVATE KERNEL) +target_compile_definitions(Kernel.1 PRIVATE + KERNEL +) + +# Use custom linker script with alignment fixes +pico_set_linker_script(Kernel.1 ${CMAKE_CURRENT_SOURCE_DIR}/Kernel/linker.ld) + +# Generate .uf2, .bin, .hex outputs pico_add_extra_outputs(Kernel.1) +# ------------------------------------------------------------------------------ +# Stripped Kernel for Production +# ------------------------------------------------------------------------------ + add_custom_target(Kernel.elf ALL COMMAND arm-none-eabi-objcopy --strip-symbol=__flash_data_2 @@ -42,5 +144,19 @@ add_custom_target(Kernel.elf ALL --strip-symbol=__flash_data_4 --strip-symbol=__flash_data_5 --strip-symbol=__flash_base - Kernel.1.elf Kernel.elf - DEPENDS Kernel.1) + Kernel.1.elf Kernel.elf + DEPENDS Kernel.1 + COMMENT "Creating stripped Kernel.elf" +) + +# ------------------------------------------------------------------------------ +# Summary +# ------------------------------------------------------------------------------ + +message(STATUS "--------------------------------------------------------------") +message(STATUS "PicoOS v${PROJECT_VERSION}") +message(STATUS " Platform: RP2040 (Raspberry Pi Pico)") +message(STATUS " Build type: ${CMAKE_BUILD_TYPE}") +message(STATUS " C Standard: ${CMAKE_C_STANDARD}") +message(STATUS " C++ Standard: ${CMAKE_CXX_STANDARD}") +message(STATUS "--------------------------------------------------------------") diff --git a/Docs/Emulation_Status.md b/Docs/Emulation_Status.md new file mode 100644 index 0000000..34dc21f --- /dev/null +++ b/Docs/Emulation_Status.md @@ -0,0 +1,14 @@ +# Emulation Status (Deferred) + +As of Dec 2025, emulation for the RP2040 SoC is not currently supported in this project. + +## QEMU +* **Status**: Unsupported in mainline. +* **Issues**: Mainline QEMU does not have an official RP2040 model. Forks exist (e.g., `raspberrypi/qemu`) but are often outdated or require complex build setups that are out of scope for this project's standard dev environment. + +## Renode +* **Status**: Technically supported but unstable in this environment. +* **Issues**: We encountered socket binding errors (`AddressAlreadyInUse`) and reliable headless logging issues when integrating with the CI/Test scripts. + +## Future Path +Once mainline QEMU adds RP2040 support (specifically dual-core M0+ and correct XIP flash emulation), we will re-enable emulation. diff --git a/Docs/InterruptSafety.md b/Docs/InterruptSafety.md new file mode 100644 index 0000000..213ff7d --- /dev/null +++ b/Docs/InterruptSafety.md @@ -0,0 +1,46 @@ +# Interrupt Safety in PicoOS + +This document outlines the safety of calling various kernel functions and using synchronization primitives from within Interrupt Service Routines (ISRs). + +## Synchronization Primitives + +| Primitive | ISR Safe? | Notes | +| :--- | :--- | :--- | +| `HardwareSpinLock` | **YES** | Safe to use. Note that it does **not** disable interrupts locally, so care must be taken to avoid deadlocks if key resources are shared with the thread on the *same* core. | +| `SoftwareSpinLock` | **YES** | Safe. Uses local interrupt masking to preventing recursion on the same core. | +| `SoftwareMutex` | **NO** | Passive locking. May sleep/yield, which is illegal in an ISR. | +| `MaskedInterruptGuard` | **N/A** | Used to disable interrupts. Safe to construct in ISR (no-op or redundant), but intended for Thread mode. | + +## Memory Allocation + +| Function | ISR Safe? | Notes | +| :--- | :--- | :--- | +| `kmalloc` / `kfree` | **NO** | Uses `malloc_mutex` (`SoftwareMutex`). | +| `PageAllocator::allocate` | **NO** | Uses `page_allocator_mutex` (`SoftwareMutex`). | +| `GlobalMemoryAllocator` | **NO** | Uses `malloc_mutex`. | +| `MemoryAllocator::allocate_eternal` | **NO** | Uses `malloc_mutex`. | + +**Recommendation**: Perform all memory allocation in Thread mode. If an ISR needs memory, pre-allocate it in the initialization phase or use a lock-free structure (e.g., ring buffer) populated by a worker thread. + +## Logging + +| Function | ISR Safe? | Notes | +| :--- | :--- | :--- | +| `dbgln` | **YES** | Uses `dbgln_mutex`. *Wait*, checking implementation... `dbgln` uses `KernelMutex` which is `SoftwareMutex`. **NO**, it is not safe if it blocks. logic might deadlock if interrupted thread holds the lock. | +| `kprintf` | **NO** | Same as `dbgln`. | + +**Warning**: Using `dbgln` in an ISR while the main thread is printing can cause a deadlock. Use with caution or only for fatal errors (`panic` overrides locks). + +## Scheduler + +| Function | ISR Safe? | Notes | +| :--- | :--- | :--- | +| `Scheduler::trigger` | **YES** | Safe. Sets PendSV bit to request a schedule. | +| `Scheduler::schedule` | **NO** | Should only be called by `pendsv` handler. | +| `Thread::wakeup` | **YES** | Safe. Adds thread to queue and triggers scheduler. | + +## General Rules + +1. **Do not block**: Never use blocking synchronization (Mutex, Semaphores) in an ISR. +2. **Do not allocate**: Avoid heap operations. +3. **Quick execution**: Keep ISRs short. Offload work to `WorkerThread` or unblock a driver thread. diff --git a/Docs/architecture/filesystem.md b/Docs/architecture/filesystem.md new file mode 100644 index 0000000..380268c --- /dev/null +++ b/Docs/architecture/filesystem.md @@ -0,0 +1,156 @@ +# FileSystem Architecture + +PicoOS implements a Virtual File System (VFS) abstraction to support different storage backends. + +## Architecture Overview + +```mermaid +graph TD + subgraph Userland["User Process"] + App[Application] + end + + subgraph Kernel["Kernel VFS Layer"] + VFS[VirtualFile] + VFH[VirtualFileHandle] + VFD[VirtualDirectory] + end + + subgraph Backends["File System Backends"] + MFS[MemoryFileSystem
RAM-based] + DFS[DeviceFileSystem
/dev] + FFS[FlashFileSystem
Read-only] + end + + subgraph Hardware["Devices"] + UART[UART0 Console] + Flash[Flash Memory] + end + + App -->|open,read,write| VFH + VFH --> VFS + VFS --> MFS + VFS --> DFS + VFS --> FFS + DFS --> UART + FFS --> Flash +``` + +--- + +## VFS Components + +### VirtualFile +The base class for all file nodes. + +| Property | Type | Description | +| :--- | :--- | :--- | +| `m_mode` | `ModeFlags` | Regular, Directory, or Device | +| `m_size` | `u32` | File size in bytes | +| `m_device_id` | `u32` | Device identifier (if device file) | +| `m_ino` | `u32` | Inode number | + +### VirtualFileHandle +Represents an open instance of a file used by a process. +- Tracks read/write offsets +- Must be closed to prevent memory leaks +- Each `open()` creates a new handle + +### VirtualDirectory +A special `VirtualFile` containing a map of child entries. +```cpp +HashMap m_entries; +``` + +--- + +## File System Implementations + +### 1. MemoryFileSystem (TmpFS) + +```mermaid +graph LR + Root["/"] --> Dev["/dev"] + Root --> Bin["/bin"] + Bin --> Shell["Shell.elf"] + Bin --> Example["Example.elf"] +``` + +- **Location**: RAM +- **Persistence**: None (lost on reboot) +- **Use Case**: Temporary files, runtime directories + +### 2. DeviceFileSystem (`/dev`) + +Maps special character devices to file nodes. + +| Device | Path | Description | +| :--- | :--- | :--- | +| Console | `/dev/tty` | UART0 input/output | + +**Console Implementation**: +- `read()`: Blocks until data available in UART RX FIFO +- `write()`: Sends data to UART TX FIFO + +### 3. FlashFileSystem + +- **Location**: Flash memory (XIP) +- **Persistence**: Read-only, embedded at build time +- **Generation**: `Tools/ElfEmbed` tool creates embedded ELF sections + +**Contents**: +``` +/bin/Shell.elf +/bin/Example.elf +/bin/Editor.elf +``` + +--- + +## File Operations Flow + +```mermaid +sequenceDiagram + participant App as Application + participant Kernel as Kernel + participant VFS as VirtualFile + participant Handle as VirtualFileHandle + + App->>Kernel: open("/dev/tty", O_RDWR) + Kernel->>VFS: lookup("/dev/tty") + VFS-->>Kernel: VirtualFile* + Kernel->>Handle: create_handle() + Handle-->>Kernel: VirtualFileHandle& + Kernel-->>App: fd = 3 + + App->>Kernel: write(3, "hello", 5) + Kernel->>Handle: write(bytes) + Handle->>VFS: (forward to device) + VFS-->>Handle: 5 bytes written + Handle-->>Kernel: 5 + Kernel-->>App: 5 + + App->>Kernel: close(3) + Kernel->>Handle: delete handle +``` + +--- + +## Key Files + +| File | Description | +| :--- | :--- | +| [VirtualFileSystem.hpp](../../Kernel/FileSystem/VirtualFileSystem.hpp) | Base VFS classes | +| [MemoryFileSystem.cpp](../../Kernel/FileSystem/MemoryFileSystem.cpp) | RAM filesystem | +| [DeviceFileSystem.cpp](../../Kernel/FileSystem/DeviceFileSystem.cpp) | Device node mapping | +| [FlashFileSystem.cpp](../../Kernel/FileSystem/FlashFileSystem.cpp) | Embedded flash files | +| [ConsoleDevice.cpp](../../Kernel/ConsoleDevice.cpp) | TTY implementation | + +--- + +## Known Limitations + +- No subdirectory creation at runtime +- No file deletion support +- Flash filesystem is read-only +- Limited error reporting diff --git a/Docs/architecture/memory.md b/Docs/architecture/memory.md new file mode 100644 index 0000000..fd87c3c --- /dev/null +++ b/Docs/architecture/memory.md @@ -0,0 +1,70 @@ +# Memory Management & Security + +PicoOS utilizes a robust set of memory management and security features tailored for the RP2040 (Cortex-M0+). Despite lacking an MMU (Memory Management Unit), it implements strong isolation and safety mechanisms. + +## Memory Allocators + +PicoOS uses a two-level memory allocation architecture: + +### 1. Physical Page Allocator (Buddy System) +* **File**: `Kernel/PageAllocator.cpp` +* **Algorithm**: Binary Buddy System +* **Unit**: 4KB Pages +* **Total Managed Memory**: ~260KB (excluding bootloader and stack scratch areas) +* **Use Case**: Allocating large, contiguous physical memory blocks for the Kernel Heap, Process Stacks, or File Buffers. + +The Page Allocator manages the raw SRAM. It tracks usage using a `SortedSet` of allocated ranges, allowing for detailed debugging and leak detection. + +### 2. Global Memory Allocator (Kernel Heap) +* **File**: `Kernel/GlobalMemoryAllocator.cpp` +* **Algorithm**: Free-List (Variable size) +* **Features**: + * **Dynamic Growth**: Starts with a small initial heap (16KB) and requests additional pages from the Page Allocator as needed. + * **Thread Safety**: Protected by `malloc_mutex` (Software Mutex) with `ScopedLock`. + * **Standard Compliance**: Implements `malloc`, `free`, `calloc`, `realloc`, and `reallocarray` (with overflow protection). + * **Eternal Allocation**: `allocate_eternal()` for persistent kernel structures that should not be tracked by leak detectors. + +--- + +## Security Features + +### Memory Protection Unit (MPU) +Since the Cortex-M0+ lacks virtual memory, PicoOS uses the MPU to enforce privilege levels: +* **Supervisor Mode (Kernel)**: Full access to all memory and peripherals. +* **User Mode (Userland)**: + * **Code**: Execute-only (XOM) from RAM or Flash. + * **Data**: Read/Write access only to its own stack and heap. + * **No Access**: Blocked from accessing Kernel RAM, Peripherals (except specific allowed zones), or other processes. + +### Stack Smashing Protection (SSP) +PicoOS implements GCC's `-fstack-protector-strong` to detect buffer overflows. +* **Hardware Randomization**: The stack canary (`__stack_chk_guard`) is seeded at boot using the RP2040's hardware Ring Oscillator (ROSC) random bit generator (`init_stack_guard()`). +* **Detection**: If a function return address is overwritten, the corrupted canary triggers `__stack_chk_fail`, causing a Kernel Panic. + +### Exception Handling +* **HardFault Handler**: Differentiates between User and Kernel faults. + * **User Fault**: Kills the offending thread/process but keeps the OS running. + * **Kernel Fault**: Panics the system with a register dump and stack trace for debugging. + +--- + +## Memory Map (RP2040) + +| Region | Address | Size | Description | +| :--- | :--- | :--- | :--- | +| **Bootrom** | `0x00000000` | 16 KB | Internal Read-Only Bootloader | +| **Flash (XIP)**| `0x10000000` | 2 MB | Operating System Code & Read-Only Data | +| **SRAM** | `0x20000000` | 264 KB | Main System Memory | +| **Peripherals**| `0x40000000` | | Memory Mapped IO (UART, DMA, FIFO, etc.) | +| **SIO** | `0xD0000000` | | Single Cycle IO (Spinlocks, CPUID, GPIO) | +| **PPB** | `0xE0000000` | | ARM Core Peripherals (NVIC, SysTick, MPU) | + +### SRAM Layout + +| Start | usage | +| :--- | :--- | +| `0x20000000` | **Kernel Data** (.data, .bss) | +| `0x20002xxx` | **Kernel Heap** (Dynamically grows upwards) | +| ... | **Free Memory** (Managed by PageAllocator) | +| `0x20040000` | **Scratch X** (Core 1 Stack / Buffers) | +| `0x20041000` | **Scratch Y** (Core 0 Stack / Buffers) | diff --git a/Docs/architecture/scheduler.md b/Docs/architecture/scheduler.md new file mode 100644 index 0000000..3fbc00c --- /dev/null +++ b/Docs/architecture/scheduler.md @@ -0,0 +1,149 @@ +# Scheduler Architecture + +The PicoOS scheduler is a **preemptive, round-robin** scheduler designed for the RP2040's dual-core Cortex-M0+. + +## Overview + +```mermaid +graph TD + subgraph Kernel + A[SysTick Interrupt] --> B[PendSV Trigger] + B --> C[scheduler_next] + C --> D{Active Thread?} + D -->|Yes, not masked| E[Re-queue Thread] + D -->|Yes, masked| F[Move to Dangling] + D -->|No| G[Skip] + E --> H[Choose Next Thread] + F --> H + G --> H + H --> I{Queued Threads?} + I -->|Yes| J[Dequeue Next] + I -->|No| K[Default Thread] + J --> L[Setup MPU] + K --> L + L --> M[Return Context] + end +``` + +--- + +## Key Components + +### Thread States + +| State | Description | +| :--- | :--- | +| **Queued** | Ready to run, waiting in `m_queued_threads` | +| **Active** | Currently executing (`m_active_thread`) | +| **Masked** | Blocked, won't be scheduled (`m_masked_from_scheduler = true`) | +| **Dangling** | Awaiting cleanup (last reference) | + +### Thread Queues + +```cpp +CircularQueue, 16> m_queued_threads; // Ready threads +CircularQueue, 16> m_dangling_threads; // Awaiting destruction +RefPtr m_active_thread; // Currently running +``` + +--- + +## Scheduling Algorithm + +1. **Timer Interrupt** (SysTick): Fires periodically based on `systick_hw->rvr`. +2. **PendSV Set**: `isr_systick()` sets the PendSV pending bit. +3. **Context Switch**: `scheduler_next()` is invoked by PendSV handler. +4. **Save Context**: Active thread's registers are stashed. +5. **Choose Next**: + - If dangling threads need cleanup → run default thread + - If queue is empty → run default thread + - Otherwise → dequeue next thread +6. **Restore Context**: Load new thread's registers, return from exception. + +--- + +## Special Threads + +### Default Thread +A kernel thread that runs when no other work is available. Its job: +- Clean up dangling threads (drop last reference) +- Immediately trigger reschedule + +### Fallback Thread +A simpler backup that runs if the default thread is blocked (e.g., waiting on a mutex). + +### Dummy Thread +Used only during boot to enter the scheduler for the first time. + +--- + +## Preemption + +Preemption is **timer-based** via SysTick: +- **Normal Mode**: `systick_hw->rvr = 0x000f0000` (~1ms at 125MHz) +- **Slow Mode**: `systick_hw->rvr = 0x00f00000` (~16ms, for debugging) + +Threads can also **yield voluntarily**: +```cpp +Scheduler::the().trigger(); +``` + +--- + +## Memory Protection + +Before returning to userland, the scheduler configures the MPU: + +```cpp +setup_mpu(m_active_thread->m_regions); +``` + +Each thread has a list of `MPU::Region` entries defining: +- Kernel Flash (read-only, execute) +- User RAM (read-write, no execute) +- ROM tables (read-only) + +--- + +## Privilege Levels + +The scheduler sets the `CONTROL` register based on thread type: + +| Thread Type | CONTROL | Privilege | +| :--- | :---: | :--- | +| Kernel (`m_privileged = true`) | `0b10` | Privileged (full access) | +| Userland (`m_privileged = false`) | `0b11` | Unprivileged (MPU enforced) | + +--- + +## Initialization Flow + +```mermaid +sequenceDiagram + participant main as main() + participant loop as Scheduler::loop() + participant dummy as Dummy Thread + participant sched as scheduler_next() + + main->>loop: Call loop() + loop->>loop: Create default_thread + loop->>loop: Create fallback_thread + loop->>dummy: Create dummy_thread + loop->>dummy: Set as active + loop->>dummy: Restore context + dummy->>dummy: Enable mutexes + dummy->>sched: trigger() + sched->>sched: Mark dummy as masked + sched->>sched: Schedule first real thread +``` + +--- + +## Key Files + +| File | Purpose | +| :--- | :--- | +| [Scheduler.hpp](../../Kernel/Threads/Scheduler.hpp) | Class definition | +| [Scheduler.cpp](../../Kernel/Threads/Scheduler.cpp) | Implementation | +| [Thread.hpp](../../Kernel/Threads/Thread.hpp) | Thread class | +| [Context.S](../../Kernel/Context.S) | Assembly context switch | diff --git a/Docs/architecture/synchronization.md b/Docs/architecture/synchronization.md new file mode 100644 index 0000000..89d97a5 --- /dev/null +++ b/Docs/architecture/synchronization.md @@ -0,0 +1,188 @@ +# Synchronization Primitives + +As of v0.3.1, PicoOS includes a suite of synchronization primitives designed for multi-core (SMP) safety on the RP2040. + +## Overview + +```mermaid +graph LR + subgraph Hardware["RP2040 SIO"] + HW[Hardware Spinlock
32 registers] + end + + subgraph Software["Kernel Primitives"] + SW[SoftwareSpinLock] + MX[SoftwareMutex] + WQ[WaitingThreadQueue] + end + + HW --> SW + HW --> MX + SW --> WQ + MX --> WQ +``` + +The RP2040 has two cores (Core 0 and Core 1). To support SMP scheduling safely, we use hardware-backed locking mechanisms. + +--- + +## 🔒 HardwareSpinLock + +A wrapper around the RP2040's hardware spinlock peripheral (`SIO`). + +| Property | Value | +| :--- | :--- | +| **Hardware Support** | 32 atomic spinlock registers | +| **Address Base** | `0xD0000100` | +| **Power Optimization** | `wfe`/`sev` instructions | + +### Lock Acquisition Flow + +```mermaid +flowchart TD + A[lock()] --> B[Disable Interrupts] + B --> C{Read spinlock reg} + C -->|Non-zero| D[Got lock! ✓] + C -->|Zero| E[wfe - sleep] + E --> C + D --> F[Memory Barrier] + F --> G[Return] +``` + +### Usage +```cpp +HardwareSpinLock lock((volatile u32*)0xD0000100); +lock.lock(); +// Critical section +lock.unlock(); +``` + +--- + +## 🌀 SoftwareSpinLock + +A scalable spinlock that allows more locks than the 32 hardware slots. + +| Property | Value | +| :--- | :--- | +| **Internal State** | `m_locked` boolean + `m_owning_core` | +| **Protection** | Shared `HardwareSpinLock` | +| **Deadlock Detection** | Panics if same core re-locks | + +### How It Works + +1. Acquires the shared hardware lock +2. Checks if `m_locked == false` +3. If yes: sets `m_locked = true`, records core ID, releases hw lock +4. If no: releases hw lock, calls `wfe`, retries + +### Usage +```cpp +// Initialize once at boot +static HardwareSpinLock s_shared_hw_lock((volatile u32*)0xD0000100); +SoftwareSpinLock::initialize(s_shared_hw_lock); + +// In code (interrupts MUST be disabled) +SoftwareSpinLock my_lock; +{ + MaskedInterruptGuard guard; // Disables interrupts + my_lock.lock(); + + // Critical section + + my_lock.unlock(); +} +``` + +> [!WARNING] +> Calling `lock()` with interrupts enabled will trigger an assertion failure. + +--- + +## 🛑 SoftwareMutex + +A blocking synchronization primitive for higher-level kernel usage. + +| Property | Value | +| :--- | :--- | +| **Behavior** | Blocks (yields to scheduler) | +| **Waiting Queue** | `CircularQueue` | +| **Owning Thread** | Tracked via `m_owner` | + +### Lock/Unlock Flow + +```mermaid +sequenceDiagram + participant T1 as Thread A + participant MX as SoftwareMutex + participant T2 as Thread B + participant Sched as Scheduler + + T1->>MX: lock() + MX-->>T1: Acquired (m_owner = A) + + T2->>MX: lock() + MX->>MX: Already held + MX->>Sched: Block Thread B + MX-->>T2: (waiting) + + T1->>MX: unlock() + MX->>T2: Wake Thread B + MX-->>T2: Acquired (m_owner = B) +``` + +### Usage +```cpp +SoftwareMutex mutex; +mutex.initialize(some_hw_spinlock); + +mutex.lock(); +// Protected resource access +mutex.unlock(); +``` + +> [!IMPORTANT] +> Interrupts **MUST be enabled** when using `SoftwareMutex`. It blocks the thread, which requires the scheduler. + +--- + +## 🚦 WaitingThreadQueue + +A helper class to manage lists of blocked threads. + +- **Thread Safety**: Protected by injected `HardwareSpinLock` +- **Capacity**: Up to 32 waiting threads +- **Operations**: `enqueue()`, `dequeue()`, `is_empty()` + +--- + +## Global Locks + +Defined and initialized in [Kernel/main.cpp](../../Kernel/main.cpp): + +| Lock | Purpose | Type | +| :--- | :--- | :--- | +| `malloc_mutex` | Global heap allocator | `SoftwareMutex` | +| `page_allocator_mutex` | Physical page allocation | `SoftwareMutex` | +| `dbgln_mutex` | Serial output (prevents interleave) | `SoftwareMutex` | + +--- + +## Comparison Table + +| Primitive | Blocking | Interrupts | Use Case | +| :--- | :---: | :---: | :--- | +| `HardwareSpinLock` | No (spins) | Disabled | Low-level, inter-core | +| `SoftwareSpinLock` | No (spins) | Disabled | Short critical sections | +| `SoftwareMutex` | Yes (yields) | Enabled | I/O, allocators, longer holds | + +--- + +## Key Files + +| File | Description | +| :--- | :--- | +| [HardwareSpinLock.hpp](../../Kernel/Synchronization/HardwareSpinLock.hpp) | Raw hardware lock wrapper | +| [SoftwareSpinLock.cpp](../../Kernel/Synchronization/SoftwareSpinLock.cpp) | Scalable spinlock with deadlock detection | +| [SoftwareMutex.cpp](../../Kernel/Synchronization/SoftwareMutex.cpp) | Blocking mutex implementation | +| [WaitingThreadQueue.cpp](../../Kernel/Synchronization/WaitingThreadQueue.cpp) | Thread queue management | diff --git a/Docs/architecture/system_overview.md b/Docs/architecture/system_overview.md new file mode 100644 index 0000000..de0a9ac --- /dev/null +++ b/Docs/architecture/system_overview.md @@ -0,0 +1,115 @@ +# System Overview + +PicoOS is a microkernel-inspired simple OS for the memory-constrained RP2040 microcontroller. + +## Architecture Diagram + +```mermaid +graph TB + subgraph Hardware["RP2040 Hardware"] + Flash[Flash XIP
2MB] + RAM[SRAM
264KB] + SIO[SIO / Spinlocks] + UART[UART0] + MPU[Memory Protection Unit] + end + + subgraph Kernel["Kernel Space"] + Sched[Scheduler] + VFS[Virtual File System] + Sync[Synchronization
HW/SW Spinlocks] + Loader[ELF Loader] + SysCall[System Call Handler] + end + + subgraph Userland["User Space"] + Shell[Shell] + Editor[Editor] + Example[Example App] + LibC[LibC] + end + + Flash --> Kernel + RAM --> Kernel + RAM --> Userland + SIO --> Sync + UART --> VFS + MPU --> Sched + + LibC --> SysCall + SysCall --> VFS + SysCall --> Sched + Loader --> Flash +``` + +--- + +## Memory Model + +The RP2040 (Cortex-M0+) lacks a Memory Management Unit (MMU), meaning there is no virtual memory pagination in the traditional sense. All addresses are physical. + +### Memory Map + +| Region | Address Range | Size | Usage | +| :--- | :--- | :--- | :--- | +| Flash (XIP) | `0x10000000 - 0x101FFFFF` | 2 MB | Kernel code, embedded files | +| SRAM | `0x20000000 - 0x20041FFF` | 264 KB | Kernel heap, user processes | +| ROM | `0x00000000 - 0x00004000` | 16 KB | RP2040 bootrom | +| Peripherals | `0x40000000+` | - | UART, SPI, GPIO, etc. | + +### Kernel Space +* Resides in Flash (XIP) and specific RAM regions. +* Runs in **Handler Mode** (interrupts) or Privileged **Thread Mode**. +* Has full access to all hardware. + +### Userland +* Processes are loaded into RAM. +* Runs in Unprivileged **Thread Mode**. +* **Isolation**: Since there is no MMU, we use the ARM v6-M **MPU** (Memory Protection Unit) to protect kernel memory from user access. +* User stacks are aligned to 8 bytes. + +--- + +## Process Lifecycle + +```mermaid +stateDiagram-v2 + [*] --> Loading: posix_spawn() + Loading --> Ready: ELF parsed & loaded + Ready --> Running: Scheduled + Running --> Ready: Preempted + Running --> Blocked: Syscall / Mutex + Blocked --> Ready: Resource available + Running --> Terminated: exit() + Terminated --> [*]: Cleanup +``` + +--- + +## Process Management + +* **Scheduler**: Round-robin scheduler with preemption via SysTick. See [Scheduler Architecture](scheduler.md). +* **Context Switching**: Saved/Restored via `PendSV` exception. +* **Loading**: ELF executables are parsed by the kernel. + * `FlashFile` executables are mapped directly (XIP) or copied to RAM depending on section type. + +--- + +## Key Subsystems + +| Subsystem | Description | Documentation | +| :--- | :--- | :--- | +| Scheduler | Thread management and context switching | [scheduler.md](scheduler.md) | +| Synchronization | Multi-core locking primitives | [synchronization.md](synchronization.md) | +| Memory & Security | Allocators, MPU, Stack Protection | [memory.md](memory.md) | +| FileSystem | VFS, DeviceFS, FlashFS | [filesystem.md](filesystem.md) | +| System Calls | User/Kernel interface | [syscalls.md](../reference/syscalls.md) | + +--- + +## Limitations + +- **No Virtual Memory**: Swapping not possible. +- **No dynamic linking**: Statically linked ELF only. +- **Crash in Kernel = System Panic**. +- **Single address space**: All processes share physical memory (isolated by MPU). diff --git a/Docs/Debug/0016_open-issues.md b/Docs/development/debug_notes/0016_open-issues.md similarity index 100% rename from Docs/Debug/0016_open-issues.md rename to Docs/development/debug_notes/0016_open-issues.md diff --git a/Docs/Debug/0021_sometimes-we-hang-on-input.md b/Docs/development/debug_notes/0021_sometimes-we-hang-on-input.md similarity index 100% rename from Docs/Debug/0021_sometimes-we-hang-on-input.md rename to Docs/development/debug_notes/0021_sometimes-we-hang-on-input.md diff --git a/Docs/Debug/Resolved/0001_out-of-memory.md b/Docs/development/debug_notes/Resolved/0001_out-of-memory.md similarity index 100% rename from Docs/Debug/Resolved/0001_out-of-memory.md rename to Docs/development/debug_notes/Resolved/0001_out-of-memory.md diff --git a/Docs/Debug/Resolved/0002_debug-mutex-in-interrupt.md b/Docs/development/debug_notes/Resolved/0002_debug-mutex-in-interrupt.md similarity index 100% rename from Docs/Debug/Resolved/0002_debug-mutex-in-interrupt.md rename to Docs/development/debug_notes/Resolved/0002_debug-mutex-in-interrupt.md diff --git a/Docs/Debug/Resolved/0004_destroy-thread-in-scheduler.md b/Docs/development/debug_notes/Resolved/0004_destroy-thread-in-scheduler.md similarity index 100% rename from Docs/Debug/Resolved/0004_destroy-thread-in-scheduler.md rename to Docs/development/debug_notes/Resolved/0004_destroy-thread-in-scheduler.md diff --git a/Docs/Debug/Resolved/0005_interrupts-disabled-in-dummy-thread.md b/Docs/development/debug_notes/Resolved/0005_interrupts-disabled-in-dummy-thread.md similarity index 100% rename from Docs/Debug/Resolved/0005_interrupts-disabled-in-dummy-thread.md rename to Docs/development/debug_notes/Resolved/0005_interrupts-disabled-in-dummy-thread.md diff --git a/Docs/Debug/Resolved/0006_infinite-loop-in-crash-report.md b/Docs/development/debug_notes/Resolved/0006_infinite-loop-in-crash-report.md similarity index 100% rename from Docs/Debug/Resolved/0006_infinite-loop-in-crash-report.md rename to Docs/development/debug_notes/Resolved/0006_infinite-loop-in-crash-report.md diff --git a/Docs/Debug/Resolved/0007_allocations-in-system-handler.md b/Docs/development/debug_notes/Resolved/0007_allocations-in-system-handler.md similarity index 100% rename from Docs/Debug/Resolved/0007_allocations-in-system-handler.md rename to Docs/development/debug_notes/Resolved/0007_allocations-in-system-handler.md diff --git a/Docs/Debug/Resolved/0008_interrupts-disabled.md b/Docs/development/debug_notes/Resolved/0008_interrupts-disabled.md similarity index 100% rename from Docs/Debug/Resolved/0008_interrupts-disabled.md rename to Docs/development/debug_notes/Resolved/0008_interrupts-disabled.md diff --git a/Docs/Debug/Resolved/0009_second-syscall-fails.md b/Docs/development/debug_notes/Resolved/0009_second-syscall-fails.md similarity index 100% rename from Docs/Debug/Resolved/0009_second-syscall-fails.md rename to Docs/development/debug_notes/Resolved/0009_second-syscall-fails.md diff --git a/Docs/Debug/Resolved/0010_refcount-of-default-thread.md b/Docs/development/debug_notes/Resolved/0010_refcount-of-default-thread.md similarity index 100% rename from Docs/Debug/Resolved/0010_refcount-of-default-thread.md rename to Docs/development/debug_notes/Resolved/0010_refcount-of-default-thread.md diff --git a/Docs/Debug/Resolved/0011_ref-ptr-is-broken.md b/Docs/development/debug_notes/Resolved/0011_ref-ptr-is-broken.md similarity index 100% rename from Docs/Debug/Resolved/0011_ref-ptr-is-broken.md rename to Docs/development/debug_notes/Resolved/0011_ref-ptr-is-broken.md diff --git a/Docs/Debug/Resolved/0012_blocking-default-thread.md b/Docs/development/debug_notes/Resolved/0012_blocking-default-thread.md similarity index 100% rename from Docs/Debug/Resolved/0012_blocking-default-thread.md rename to Docs/development/debug_notes/Resolved/0012_blocking-default-thread.md diff --git a/Docs/Debug/Resolved/0013_deadlock.md b/Docs/development/debug_notes/Resolved/0013_deadlock.md similarity index 100% rename from Docs/Debug/Resolved/0013_deadlock.md rename to Docs/development/debug_notes/Resolved/0013_deadlock.md diff --git a/Docs/Debug/Resolved/0014_lock-in-boot.md b/Docs/development/debug_notes/Resolved/0014_lock-in-boot.md similarity index 100% rename from Docs/Debug/Resolved/0014_lock-in-boot.md rename to Docs/development/debug_notes/Resolved/0014_lock-in-boot.md diff --git a/Docs/Debug/Resolved/0015_crash-on-new-process.md b/Docs/development/debug_notes/Resolved/0015_crash-on-new-process.md similarity index 100% rename from Docs/Debug/Resolved/0015_crash-on-new-process.md rename to Docs/development/debug_notes/Resolved/0015_crash-on-new-process.md diff --git a/Docs/Debug/Resolved/0017_crash-on-system-call.md b/Docs/development/debug_notes/Resolved/0017_crash-on-system-call.md similarity index 100% rename from Docs/Debug/Resolved/0017_crash-on-system-call.md rename to Docs/development/debug_notes/Resolved/0017_crash-on-system-call.md diff --git a/Docs/Debug/Resolved/0018_deadlock.md b/Docs/development/debug_notes/Resolved/0018_deadlock.md similarity index 100% rename from Docs/Debug/Resolved/0018_deadlock.md rename to Docs/development/debug_notes/Resolved/0018_deadlock.md diff --git a/Docs/Debug/Resolved/0019_page-allocator.md b/Docs/development/debug_notes/Resolved/0019_page-allocator.md similarity index 100% rename from Docs/Debug/Resolved/0019_page-allocator.md rename to Docs/development/debug_notes/Resolved/0019_page-allocator.md diff --git a/Docs/Debug/Resolved/0020_assert-on-exit.md b/Docs/development/debug_notes/Resolved/0020_assert-on-exit.md similarity index 100% rename from Docs/Debug/Resolved/0020_assert-on-exit.md rename to Docs/development/debug_notes/Resolved/0020_assert-on-exit.md diff --git a/Docs/Debug/Resolved/0022_fault-in-editor.md b/Docs/development/debug_notes/Resolved/0022_fault-in-editor.md similarity index 100% rename from Docs/Debug/Resolved/0022_fault-in-editor.md rename to Docs/development/debug_notes/Resolved/0022_fault-in-editor.md diff --git a/Docs/Debug/Resolved/0023_memmove-is-buggy.md b/Docs/development/debug_notes/Resolved/0023_memmove-is-buggy.md similarity index 100% rename from Docs/Debug/Resolved/0023_memmove-is-buggy.md rename to Docs/development/debug_notes/Resolved/0023_memmove-is-buggy.md diff --git a/Docs/development/workflow.md b/Docs/development/workflow.md new file mode 100644 index 0000000..c89f53d --- /dev/null +++ b/Docs/development/workflow.md @@ -0,0 +1,31 @@ +# Development Workflow + +## Adding a New System Call + +1. **Define ID**: Add a new `_SC_name` constant in `Kernel/Interface/System.hpp`. +2. **Kernel Implementation**: + * Add declaration to `Thread` class in `Kernel/Threads/Thread.hpp`. + * Implement `sys$name` in `Kernel/Threads/Thread.cpp`. + * Add case to `Thread::syscall` dispatch switch. +3. **Userland Wrapper**: + * Implement function in `Userland/LibC/sys/syscalls.cpp` using `syscall0`...`syscall3` macros. + * Expose header in `Userland/LibC` (or standard headers). + +## Running Tests + +### Standard Library Tests +The core data structures are tested on the host machine. +1. Navigate to `Tests/` or use the main build. +2. (See `Tests/CMakeLists.txt` for details). + +### Kernel Verification +Currently requires `QEMU` or actual hardware. +1. Build `Kernel.1.uf2`. +2. Flash to Pico. +3. Observe output via UART (`inv tty`). + +## Code Style +* Use `PascalCase` for classes/structs. +* Use `snake_case` for functions and variables. +* Use `m_` prefix for member variables. +* Use `AK` (SerenityOS) style references (`RefPtr`, `OwnPtr`) where possible. diff --git a/Docs/getting_started/debugging.md b/Docs/getting_started/debugging.md new file mode 100644 index 0000000..b898805 --- /dev/null +++ b/Docs/getting_started/debugging.md @@ -0,0 +1,56 @@ +# Debugging PicoOS + +Effective debugging is essential for OS development. PicoOS supports hardware debugging via **SWD** (Serial Wire Debug) using a **PicoProbe** or similar adapter. + +## Hardware Setup + +**Required**: +1. **Target Pico**: The one running PicoOS. +2. **Debug Probe**: Another Pico running [picoprobe](https://github.com/raspberrypi/picoprobe) firmware, or a J-Link. +3. **Wiring**: + * Probe GND -> Target GND + * Probe GP2 (SWCLK) -> Target SWCLK + * Probe GP3 (SWDIO) -> Target SWDIO + * Probe GP4 (UART TX) -> Target GP1 (UART RX) + * Probe GP5 (UART RX) -> Target GP0 (UART TX) + +## Using OpenOCD + +We use open-source OpenOCD to interface with the probe. + +1. **Start OpenOCD**: + ```bash + inv probe + ``` + *This runs: `openocd -f interface/picoprobe.cfg -f target/rp2040.cfg`* + +## Using GDB + +Once OpenOCD is running (port 3333): + +1. **Start GDB**: + ```bash + inv dbg + ``` + *This loads `build/Kernel.elf` and connects to localhost:3333.* + +2. **GDB Helper Commands**: + The `inv dbg` task sets up helpful macros: + * `rebuild`: Runs ninja, reloads symbols, resets target. + * `si_and_dis`: Step instruction and disassemble. + +## VSCode Integration + +The project includes VSCode configuration files for a seamless experience. + +### Setup +1. Install the **Cortex-Debug** extension. +2. Install the **C/C++** extension. +3. Ensure `arm-none-eabi-gdb` is in your PATH. + +### Launch Configurations +* **Cortex Debug: Attach**: Connects to a running OpenOCD session or starts one, attaches to the Kernel, and pauses execution. + * Select this from the logical "Run and Debug" tab. + * Press **F5**. + +It maps source lines to assembly steps, allows variable inspection, and breakpoint management. diff --git a/Docs/getting_started/index.md b/Docs/getting_started/index.md new file mode 100644 index 0000000..b4deb94 --- /dev/null +++ b/Docs/getting_started/index.md @@ -0,0 +1,104 @@ +# Getting Started with PicoOS + +## Prerequisites + +To build and develop PicoOS, you need the following tools: + +### Cross-Compiler +* **ARM GNU Toolchain**: `arm-none-eabi-gcc`, `arm-none-eabi-g++`, `arm-none-eabi-gdb`. + * *macOS*: `brew install --cask arm-gnu-toolchain` (or download from ARM website). + * *Linux*: `sudo apt install gcc-arm-none-eabi gdb-multiarch` (or package manager equivalent). + +### Build Tools +* **CMake** (3.19+) +* **Ninja** (Recommended generator) +* **Python 3** (for `invoke` tasks) +* **Git** + +### Dependencies +* **Pico SDK**: The project automatically fetches the SDK if not found, but setting `PICO_SDK_PATH` is recommended. +* **Python Packages**: `invoke` (`pip install invoke`). +* **fmt**: C++ formatting library (often installed via system package manager or CMake will look for it). + +--- + +## Setting Up + +1. **Clone the Repository**: + ```bash + git clone https://github.com/asynts/pico-os.git + cd pico-os + ``` + +2. **Install Python Dependencies**: + ```bash + pip3 install invoke + ``` + +--- + +## Building the Project + +We use `cmake` and `ninja`. + +1. **Create a Build Directory**: + ```bash + mkdir build + cd build + ``` + +2. **Configure CMake**: + ```bash + # You might need to specify PICO_SDK_PATH if not in environment + cmake .. -G Ninja + ``` + +3. **Build**: + ```bash + ninja + ``` + + This will generate: + * `build/Kernel.elf`: ELF file for debugging (includes symbols). + * `build/Kernel.1.uf2`: UF2 firmware image for flashing. + +--- + +## Flashing + +### Using UF2 (USB Mass Storage) +1. Connect the Raspberry Pi Pico to your computer while holding the **BOOTSEL** button. +2. The Pico will appear as a USB Mass Storage device named `RPI-RP2`. +3. Run the helper task: + ```bash + inv flash + ``` + *Or manually copy the file:* + ```bash + cp build/Kernel.1.uf2 /Volumes/RPI-RP2/ + ``` + +### Using PicoProbe (Debugging) +If you have a second Pico set up as a probe: +1. Connect the Probe to the target Pico (SWD and UART). +2. Flash using GDB (see [Debugging](debugging.md)). + +--- + +## Interacting with the Shell + +PicoOS exposes a shell via the UART0 interface (GP0/TX, GP1/RX). + +1. **Determine your Serial Port**: + * *macOS*: `/dev/tty.usbmodem*` or `/dev/cu.usbmodem*` + * *Linux*: `/dev/ttyUSB*` or `/dev/ttyACM*` + +2. **Connect**: + You can use the helper task: + ```bash + inv tty + ``` + Or use `screen` / `tio` directly: + ```bash + screen /dev/tty.usbmodem123456 115200 + ``` diff --git a/Docs/demo.gif b/Docs/images/demo.gif similarity index 100% rename from Docs/demo.gif rename to Docs/images/demo.gif diff --git a/Docs/demo.mp4 b/Docs/images/demo.mp4 similarity index 100% rename from Docs/demo.mp4 rename to Docs/images/demo.mp4 diff --git a/Docs/index.md b/Docs/index.md new file mode 100644 index 0000000..3ab75f1 --- /dev/null +++ b/Docs/index.md @@ -0,0 +1,69 @@ +# PicoOS Documentation + +Welcome to the documentation for **PicoOS**, a simple operating system designed for the Raspberry Pi Pico (RP2040) microcontroller. + +--- + +## 🚀 Getting Started + +| Document | Description | +| :--- | :--- | +| [Installation & Setup](getting_started/index.md) | Prerequisites, build instructions, and flashing | +| [Debugging](getting_started/debugging.md) | Using OpenOCD, GDB, and VSCode | + +--- + +## 🏗️ Architecture + +| Document | Description | +| :--- | :--- | +| [System Overview](architecture/system_overview.md) | Memory model, kernel/userland split, MPU | +| [Scheduler](architecture/scheduler.md) | Preemptive round-robin, context switching | +| [Synchronization](architecture/synchronization.md) | Multi-core locks: `SoftwareSpinLock`, `SoftwareMutex` | +| [Memory & Security](architecture/memory.md) | Allocators, MPU, Stack Protection, Headers | +| [FileSystem](architecture/filesystem.md) | VFS, `/dev`, Flash memory | + +--- + +## 📚 Reference + +| Document | Description | +| :--- | :--- | +| [System Calls](reference/syscalls.md) | Complete syscall reference with examples | +| [Shell Commands](reference/shell.md) | Built-in shell commands | + +--- + +## 🛠️ Development + +| Document | Description | +| :--- | :--- | +| [Workflow](development/workflow.md) | Adding syscalls, running tests, code style | +| [Debug Notes](development/debug_notes/) | Historical debugging logs | + +--- + +## 📊 Quick Reference + +### Build Commands +```bash +mkdir build && cd build +cmake .. -G Ninja +ninja +``` + +### Flash Firmware +```bash +inv flash # Pico must be in BOOTSEL mode +``` + +### Connect to Shell +```bash +inv tty +``` + +### Start Debugging +```bash +inv probe # Terminal 1: OpenOCD +inv dbg # Terminal 2: GDB +``` diff --git a/Docs/reference/shell.md b/Docs/reference/shell.md new file mode 100644 index 0000000..1c992e9 --- /dev/null +++ b/Docs/reference/shell.md @@ -0,0 +1,148 @@ +# Shell Commands + +PicoOS includes a basic interactive shell (`/bin/Shell.elf`) that provides a command-line interface over UART. + +## Starting the Shell + +The shell starts automatically after boot. Connect via serial: +```bash +inv tty +# or: screen /dev/tty.usbmodem* 115200 +``` + +You'll see a prompt: +``` +> +``` + +--- + +## Built-in Commands + +### `echo [args...]` +Print arguments to the terminal. + +``` +> echo Hello World +Hello World +``` + +### `pwd` +Print the current working directory. + +``` +> pwd +/ +``` + +### `cd ` +Change the current working directory. + +``` +> cd /bin +> pwd +/bin +``` + +### `ls [path]` +List directory contents. If no path is given, lists the current directory. + +``` +> ls +Shell.elf +Example.elf +Editor.elf + +> ls /dev +tty +``` + +### `cat ` +Display the contents of a file. + +``` +> cat /test.txt +Hello from the file! +``` + +### `touch ` +Create an empty file (or update timestamps). + +``` +> touch /newfile.txt +``` + +### `stat ` +Display file information. + +``` +> stat /dev/tty +Device: 0x00010001 +Size: 0 +Mode: Device +``` + +### `exit` +Exit the shell (terminates the process). + +``` +> exit +``` + +--- + +## Running External Programs + +Any command that isn't a builtin is treated as an external executable: + +``` +> Example.elf +This output is created by '/bin/Example.elf' +We are currently executing in '/' +``` + +The shell searches for executables in the `PATH` environment variable (default: `/bin`). + +### Passing Arguments + +``` +> Example.elf arg1 arg2 +... +Arguments: + 'Example.elf' + 'arg1' + 'arg2' +``` + +--- + +## Special Characters + +| Character | Behavior | +| :---: | :--- | +| `Backspace` (0x7F) | Delete previous character | +| `Enter` | Execute command | +| `Ctrl+C` | *Not implemented* | + +--- + +## Limitations + +- **No pipes** (`|`) +- **No redirections** (`>`, `<`) +- **No background jobs** (`&`) +- **No command history** (up/down arrows) +- **No tab completion** +- **Fixed PATH** (`/bin` only) + +--- + +## Implementation Notes + +The shell is implemented in [`Userland/Shell.c`](pico-os/Userland/Shell.c). + +Key functions: +- `readline()`: Reads user input with basic line editing +- `find_executable()`: Searches PATH for programs +- `posix_spawn()`: Spawns child processes +- `wait()`: Waits for child to terminate diff --git a/Docs/reference/syscalls.md b/Docs/reference/syscalls.md new file mode 100644 index 0000000..9cb0e7f --- /dev/null +++ b/Docs/reference/syscalls.md @@ -0,0 +1,198 @@ +# System Calls + +PicoOS communicates with the kernel via `svc` (Supervisor Call) instructions. The C library (`LibC`) wraps these into standard POSIX-like functions. + +## Overview + +```mermaid +sequenceDiagram + participant User as Userland Process + participant LibC as LibC Wrapper + participant SVC as SVC Handler + participant Kernel as Kernel Thread + + User->>LibC: write(fd, buf, len) + LibC->>SVC: svc #0 (syscall) + SVC->>Kernel: sys$write(fd, buf, len) + Kernel-->>SVC: Return value + SVC-->>LibC: r0 = result + LibC-->>User: return result +``` + +--- + +## Implemented Syscalls + +| Syscall | ID | Signature | Description | +| :--- | :---: | :--- | :--- | +| `exit` | 1 | `void exit(int status)` | Terminate the current process with status code. | +| `read` | 3 | `ssize_t read(int fd, void *buf, size_t count)` | Read up to `count` bytes from file descriptor into buffer. | +| `write` | 4 | `ssize_t write(int fd, const void *buf, size_t count)` | Write `count` bytes from buffer to file descriptor. | +| `open` | 5 | `int open(const char *path, int flags, mode_t mode)` | Open or create a file. Returns file descriptor. | +| `close` | 6 | `int close(int fd)` | Close an open file descriptor. | +| `wait` | 7 | `pid_t wait(int *wstatus)` | Wait for any child process to terminate. | +| `chdir` | 12 | `int chdir(const char *path)` | Change current working directory. | +| `fstat` | 28 | `int fstat(int fd, struct stat *statbuf)` | Get file status by file descriptor. | +| `get_working_directory` | 200 | `int getcwd(char *buf, size_t size)` | Get current working directory path. | +| `posix_spawn` | 201 | `int posix_spawn(pid_t *pid, const char *path, ...)` | Spawn a new process from an ELF file. | + +--- + +## Detailed Examples + +### Reading from a File + +```c +#include +#include +#include + +int main() { + char buffer[128]; + int fd = open("/test.txt", O_RDONLY); + if (fd < 0) { + printf("Failed to open file\n"); + return 1; + } + + ssize_t nread = read(fd, buffer, sizeof(buffer) - 1); + if (nread > 0) { + buffer[nread] = '\0'; + printf("Read %d bytes: %s\n", (int)nread, buffer); + } + + close(fd); + return 0; +} +``` + +### Writing to TTY + +```c +#include +#include + +int main() { + int fd = open("/dev/tty", O_WRONLY); + const char *msg = "Hello from PicoOS!\n"; + write(fd, msg, strlen(msg)); + close(fd); + return 0; +} +``` + +### Creating a File + +```c +#include +#include + +int main() { + // O_CREAT creates the file if it doesn't exist + // Permissions: rw-r--r-- + int fd = open("/newfile.txt", O_WRONLY | O_CREAT, S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH); + if (fd >= 0) { + write(fd, "New content\n", 12); + close(fd); + } + return 0; +} +``` + +### Spawning a Child Process + +```c +#include +#include +#include + +int main() { + pid_t child_pid; + char *argv[] = { "/bin/Example.elf", "arg1", "arg2", NULL }; + char *envp[] = { "PATH=/bin", NULL }; + + int result = posix_spawn(&child_pid, "/bin/Example.elf", NULL, NULL, argv, envp); + if (result == 0) { + printf("Spawned child PID: %d\n", child_pid); + + int status; + wait(&status); // Wait for child to finish + printf("Child exited with status: %d\n", status); + } + return 0; +} +``` + +### Getting Current Directory + +```c +#include +#include + +int main() { + char *cwd = get_current_dir_name(); + if (cwd) { + printf("Current directory: %s\n", cwd); + free(cwd); + } + return 0; +} +``` + +### Changing Directory + +```c +#include +#include + +int main() { + if (chdir("/bin") == 0) { + printf("Changed to /bin\n"); + } else { + printf("chdir failed\n"); + } + return 0; +} +``` + +--- + +## Error Handling + +Syscalls return negative values on error. The `errno` global variable is set accordingly: + +| Error | Value | Description | +| :--- | :---: | :--- | +| `ENOENT` | 2 | No such file or directory | +| `ENOTDIR` | 20 | Not a directory | +| `EISDIR` | 21 | Is a directory (when writing) | +| `EACCES` | 13 | Permission denied | +| `ERANGE` | 34 | Result out of range | + +```c +#include +#include +#include + +int fd = open("/nonexistent", O_RDONLY); +if (fd < 0) { + printf("Error: %s\n", strerror(errno)); // "No such file or directory" +} +``` + +--- + +## Low-Level Syscall Interface + +For direct syscall access (bypassing LibC): + +```c +#include + +// Direct syscall wrappers +ssize_t sys$read(int fd, void *buf, size_t count); +ssize_t sys$write(int fd, const void *buf, size_t count); +int sys$open(const char *path, int flags, int mode); +int sys$close(int fd); +void sys$exit(int status); +``` diff --git a/Kernel/ConsoleDevice.cpp b/Kernel/ConsoleDevice.cpp index 4ec9e0e..cf1ac22 100644 --- a/Kernel/ConsoleDevice.cpp +++ b/Kernel/ConsoleDevice.cpp @@ -1,32 +1,30 @@ #include #include -namespace Kernel -{ - ConsoleFile::ConsoleFile() - { - Interrupt::UART::the(); - } +namespace Kernel { +ConsoleFile::ConsoleFile() { + Interrupt::UART::the(); + m_mode = ModeFlags::Device; + m_device_id = 0x00010001; +} - VirtualFile& ConsoleFileHandle::file() { return ConsoleFile::the(); } +VirtualFile &ConsoleFileHandle::file() { return m_file; } - KernelResult ConsoleFileHandle::read(Bytes bytes) - { - for (;;) { - usize nread = Interrupt::UART::the().read(bytes).must(); +KernelResult ConsoleFileHandle::read(Bytes bytes) { + for (;;) { + usize nread = Interrupt::UART::the().read(bytes).must(); - if (nread > 0) - return nread; - } - } + if (nread > 0) + return nread; + } +} - KernelResult ConsoleFileHandle::write(ReadonlyBytes bytes) - { - usize nwritten = 0; - while (bytes.size() > nwritten) { - nwritten += Interrupt::UART::the().write(bytes.slice(nwritten)).must(); - } +KernelResult ConsoleFileHandle::write(ReadonlyBytes bytes) { + usize nwritten = 0; + while (bytes.size() > nwritten) { + nwritten += Interrupt::UART::the().write(bytes.slice(nwritten)).must(); + } - return nwritten; - } + return nwritten; } +} // namespace Kernel diff --git a/Kernel/ConsoleDevice.hpp b/Kernel/ConsoleDevice.hpp index 080785f..4c501ad 100644 --- a/Kernel/ConsoleDevice.hpp +++ b/Kernel/ConsoleDevice.hpp @@ -1,37 +1,34 @@ #pragma once -#include #include +#include #include namespace Kernel { - class ConsoleFileHandle final : public VirtualFileHandle - { - public: - KernelResult read(Bytes bytes) override; - KernelResult write(ReadonlyBytes bytes) override; - - VirtualFile& file() override; - }; - - class ConsoleFile final - : public Singleton - , public VirtualFile - { - public: - VirtualFileHandle& create_handle_impl() override - { - return *new ConsoleFileHandle; - } - - void truncate() override - { - VERIFY_NOT_REACHED(); - } - - private: - friend Singleton; - ConsoleFile(); - }; -} +class ConsoleFileHandle final : public VirtualFileHandle { +public: + explicit ConsoleFileHandle(VirtualFile &file) : m_file(file) {} + + KernelResult read(Bytes bytes) override; + KernelResult write(ReadonlyBytes bytes) override; + + VirtualFile &file() override; + +private: + VirtualFile &m_file; +}; + +class ConsoleFile final : public Singleton, public VirtualFile { +public: + VirtualFileHandle &create_handle_impl() override { + return *new ConsoleFileHandle(*this); + } + + void truncate() override { VERIFY_NOT_REACHED(); } + +private: + friend Singleton; + ConsoleFile(); +}; +} // namespace Kernel diff --git a/Kernel/Exceptions.cpp b/Kernel/Exceptions.cpp new file mode 100644 index 0000000..ac819ec --- /dev/null +++ b/Kernel/Exceptions.cpp @@ -0,0 +1,81 @@ +#include +#include +#include +#include + +// Defined in Kernel/Threads/Scheduler.cpp +extern "C" Kernel::FullRegisterContext & +scheduler_next(Kernel::FullRegisterContext &context); + +// Pico SDK panic +extern "C" void __attribute__((noreturn)) panic(const char *fmt, ...); + +extern "C" { +// HardFault Handler (User Mode / Process context) +// Called when a fault occurs while the Process Stack Pointer (PSP) was in use. +Kernel::FullRegisterContext & +hard_fault_handler_cpp(Kernel::FullRegisterContext &context) { + auto &scheduler = Kernel::Scheduler::the(); + auto thread = scheduler.get_active_thread_if_avaliable(); + + if (thread) { + dbgln("\n=== HardFault in User Thread ==="); + dbgln("Thread: '{}' (PID {})", thread->m_name, + thread->m_process->m_process_id); + + // Dump registers + dbgln("R0 ={x} R1 ={x} R2 ={x} R3 ={x}", context.r0.m_storage, + context.r1.m_storage, context.r2.m_storage, context.r3.m_storage); + dbgln("R12={x} LR ={x} PC ={x} PSR={x}", context.ip.m_storage, + context.lr.m_storage, context.pc.m_storage, context.xpsr.m_storage); + dbgln("SP ={}", &context); // Approximate PSP + + // Mark thread as masked so it won't be scheduled again, preventing a fault + // loop + thread->set_masked_from_scheduler(true); + } else { + // Should not happen if we came from PSP, unless scheduler state is + // corrupted + panic("HardFault in User Mode (PSP) but no active thread!"); + } + + // Switch to next scheduled thread + return scheduler_next(context); +} + +// HardFault Handler (Kernel Mode / IRQ context) +// Called when a fault occurs while Main Stack Pointer (MSP) was in use. +void kernel_hard_fault_handler_cpp(u32 *msp, u32 lr) { + dbgln("\n=== HARD FAULT IN KERNEL MODE ==="); + dbgln("LR = {x} (EXC_RETURN)", lr); + dbgln("MSP = {}", (void *)msp); + + // Cortex-M0+ pushes 8 words to the stack on exception: + // [R0, R1, R2, R3, R12, LR, PC, xPSR] + struct ExceptionStackFrame { + u32 r0, r1, r2, r3, r12, lr, pc, xpsr; + }; + + auto *frame = reinterpret_cast(msp); + + dbgln("Exception Stack Frame:"); + dbgln(" PC = {x}", frame->pc); + dbgln(" LR = {x}", frame->lr); + dbgln(" R0 = {x}", frame->r0); + dbgln(" R1 = {x}", frame->r1); + dbgln(" R2 = {x}", frame->r2); + dbgln(" R3 = {x}", frame->r3); + dbgln(" R12 = {x}", frame->r12); + dbgln(" PSR = {x}", frame->xpsr); + + // TODO: On Cortex-M3/M4+ we would dump CFSR/HFSR here. + // Cortex-M0+ has limited fault analysis capabilities. + + dbgln("Stack Trace (Raw Dump):"); + for (int i = 0; i < 16; ++i) { + dbgln(" [{x}] = {x}", (u32)(msp + i), msp[i]); + } + + panic("Kernel HardFault - System Halted"); +} +} diff --git a/Kernel/FileSystem/DeviceFileSystem.cpp b/Kernel/FileSystem/DeviceFileSystem.cpp index 9944815..c1ee267 100644 --- a/Kernel/FileSystem/DeviceFileSystem.cpp +++ b/Kernel/FileSystem/DeviceFileSystem.cpp @@ -14,9 +14,8 @@ namespace Kernel // FIXME: Move this to ConsoleFile::ConsoleFile add_device(0x00010001, ConsoleFile::the()); - auto& tty_file = *new MemoryFile; - tty_file.m_mode = ModeFlags::Device; - tty_file.m_device_id = 0x00010001; - dev_directory.m_entries.set("tty", &tty_file); + + // Use ConsoleFile directly so stat() returns correct device info + dev_directory.m_entries.set("tty", &ConsoleFile::the()); } } diff --git a/Kernel/FileSystem/VirtualFileSystem.hpp b/Kernel/FileSystem/VirtualFileSystem.hpp index a0331ba..5e3b2bc 100644 --- a/Kernel/FileSystem/VirtualFileSystem.hpp +++ b/Kernel/FileSystem/VirtualFileSystem.hpp @@ -61,6 +61,8 @@ namespace Kernel class VirtualFileHandle { public: + virtual ~VirtualFileHandle() = default; + virtual VirtualFile& file() = 0; virtual KernelResult read(Bytes) = 0; diff --git a/Kernel/Forward.hpp b/Kernel/Forward.hpp index 96480e2..70506a2 100644 --- a/Kernel/Forward.hpp +++ b/Kernel/Forward.hpp @@ -2,9 +2,10 @@ #include -namespace Kernel -{ - using namespace Std; +namespace Kernel { +using namespace Std; - class Thread; -} +class Thread; + +void init_stack_guard(); +} // namespace Kernel diff --git a/Kernel/GlobalMemoryAllocator.cpp b/Kernel/GlobalMemoryAllocator.cpp index 2b61bca..42ce18e 100644 --- a/Kernel/GlobalMemoryAllocator.cpp +++ b/Kernel/GlobalMemoryAllocator.cpp @@ -1,125 +1,137 @@ #include -#include -#include #include +#include +#include +#include -namespace Kernel -{ - GlobalMemoryAllocator::GlobalMemoryAllocator() - : MemoryAllocator(allocate_heap()) - { - - } - - void GlobalMemoryAllocator::set_mutex_enabled(bool enabled) - { - malloc_mutex.set_enabled(enabled); - } +namespace Kernel { - Bytes GlobalMemoryAllocator::allocate_heap() - { - m_heap = PageAllocator::the().allocate(power_of_two(0x4000)).must(); - return m_heap->bytes(); - } +GlobalMemoryAllocator::GlobalMemoryAllocator() + : MemoryAllocator(allocate_heap()) { + // Initialize Stack Smashing Protection + init_stack_guard(); +} - u8* GlobalMemoryAllocator::allocate(usize size, bool debug_override, void *address) - { - VERIFY(Kernel::is_executing_in_thread_mode()); +void GlobalMemoryAllocator::set_mutex_enabled(bool enabled) { + malloc_mutex.set_enabled(enabled); +} - malloc_mutex.lock(); - u8 *retval = MemoryAllocator::allocate(size, debug_override, address); - malloc_mutex.unlock(); +u8 *GlobalMemoryAllocator::allocate_eternal(usize size) { + // Allocate without logging (debug_override=false) + // Used for persistent structures that don't need tracking or can't fail + // visibly. + return allocate(size, false, __builtin_return_address(0)); +} - return retval; - } +Bytes GlobalMemoryAllocator::allocate_heap() { + // Initial Kernel Heap (16KB). + // The heap will grow dynamically as needed via allocate(). + m_heap = PageAllocator::the().allocate(power_of_two(16 * KiB)).must(); + dbgln("GlobalMemoryAllocator: Initialized with 16KB heap at {}", + m_heap->bytes().data()); + return m_heap->bytes(); +} - void GlobalMemoryAllocator::deallocate(u8 *pointer, bool debug_override, void *address) - { - VERIFY(Kernel::is_executing_in_thread_mode()); +// Wrapper to ensure Thread Safety check and Mutex Locking +u8 *GlobalMemoryAllocator::allocate(usize size, bool debug_override, + void *address) { + VERIFY(Kernel::is_executing_in_thread_mode()); + + Kernel::ScopedMutex lock(malloc_mutex); + + u8 *ptr = MemoryAllocator::allocate(size, debug_override, address); + if (ptr) + return ptr; + + // Heap exhausted. Try to expand. + // We need at least size + sizeof(Node) bytes. + // We'll allocate in 16KB chunks to reduce fragmentation/overhead. + usize grow_size = 16 * KiB; + if (size + 32 > grow_size) { + // If request is huge, allocate enough for it (aligned to power of two). + // We calculate nearest power of two > size + usize power = 0; + while ((1u << power) < (size + 128)) + power++; + grow_size = 1 << power; + } + + // Calculate power for PageAllocator + usize power = 0; + while ((1u << power) < grow_size) + power++; + + auto block_opt = PageAllocator::the().allocate(power); + if (!block_opt.is_valid()) { + dbgln("GlobalMemoryAllocator: OOM! Failed to expand heap by {} bytes", + 1u << power); + return nullptr; + } + + auto &block = block_opt.value(); + Bytes bytes = block.bytes(); + + dbgln("GlobalMemoryAllocator: Growing heap by {} bytes at {}", bytes.size(), + bytes.data()); + add_memory(bytes); + + // Retry allocation + return MemoryAllocator::allocate(size, debug_override, address); +} - malloc_mutex.lock(); - MemoryAllocator::deallocate(pointer, debug_override, address); - malloc_mutex.unlock(); - } +void GlobalMemoryAllocator::deallocate(u8 *pointer, bool debug_override, + void *address) { + VERIFY(Kernel::is_executing_in_thread_mode()); - u8* GlobalMemoryAllocator::reallocate(u8 *pointer, usize size, bool debug_override, void *address) - { - VERIFY(Kernel::is_executing_in_thread_mode()); + Kernel::ScopedMutex lock(malloc_mutex); + MemoryAllocator::deallocate(pointer, debug_override, address); +} - malloc_mutex.lock(); - u8 *retval = MemoryAllocator::reallocate(pointer, size, debug_override, address); - malloc_mutex.unlock(); +u8 *GlobalMemoryAllocator::reallocate(u8 *pointer, usize size, + bool debug_override, void *address) { + VERIFY(Kernel::is_executing_in_thread_mode()); - return retval; - } + Kernel::ScopedMutex lock(malloc_mutex); + return MemoryAllocator::reallocate(pointer, size, debug_override, address); } -void* operator new(usize size) -{ - void *address = __builtin_return_address(0); - return Kernel::GlobalMemoryAllocator::the().allocate(size, true, address); -} -void* operator new[](usize size) -{ - void *address = __builtin_return_address(0); - return Kernel::GlobalMemoryAllocator::the().allocate(size, true, address); -} -void operator delete(void* pointer) -{ - void *address = __builtin_return_address(0); - return Kernel::GlobalMemoryAllocator::the().deallocate(reinterpret_cast(pointer), true, address); -} -void operator delete[](void* pointer) -{ - void *address = __builtin_return_address(0); - return Kernel::GlobalMemoryAllocator::the().deallocate(reinterpret_cast(pointer), true, address); -} -void operator delete(void* pointer, usize) -{ - void *address = __builtin_return_address(0); - return Kernel::GlobalMemoryAllocator::the().deallocate(reinterpret_cast(pointer), true, address); -} -void operator delete[](void* pointer, usize) -{ - void *address = __builtin_return_address(0); - return Kernel::GlobalMemoryAllocator::the().deallocate(reinterpret_cast(pointer), true, address); -} +} // namespace Kernel -extern "C" -void* malloc(usize size) -{ - void *address = __builtin_return_address(0); - return Kernel::GlobalMemoryAllocator::the().allocate(size, true, address); +// --- Standard C Library Overrides --- + +extern "C" void *malloc(usize size) { + return Kernel::GlobalMemoryAllocator::the().allocate( + size, true, __builtin_return_address(0)); } -extern "C" -void* calloc(usize nmembers, usize size) -{ - void *address = __builtin_return_address(0); - u8 *pointer = Kernel::GlobalMemoryAllocator::the().allocate(nmembers * size, true, address); +extern "C" void *calloc(usize nmembers, usize size) { + usize total_size = nmembers * size; + u8 *pointer = Kernel::GlobalMemoryAllocator::the().allocate( + total_size, true, __builtin_return_address(0)); - __builtin_memset(pointer, 0, nmembers * size); + if (pointer) + __builtin_memset(pointer, 0, total_size); - return pointer; + return pointer; } -extern "C" -void free(void *pointer) -{ - void *address = __builtin_return_address(0); - return Kernel::GlobalMemoryAllocator::the().deallocate(reinterpret_cast(pointer), true, address); +extern "C" void free(void *pointer) { + return Kernel::GlobalMemoryAllocator::the().deallocate( + reinterpret_cast(pointer), true, __builtin_return_address(0)); } -extern "C" -void* realloc(void *pointer, usize size) -{ - void *address = __builtin_return_address(0); - return Kernel::GlobalMemoryAllocator::the().reallocate(reinterpret_cast(pointer), size, true, address); +extern "C" void *realloc(void *pointer, usize size) { + return Kernel::GlobalMemoryAllocator::the().reallocate( + reinterpret_cast(pointer), size, true, __builtin_return_address(0)); } -extern "C" -void* reallocarray(void *pointer, usize nmembers, usize size) -{ - void *address = __builtin_return_address(0); - return Kernel::GlobalMemoryAllocator::the().reallocate(reinterpret_cast(pointer), nmembers * size, true, address); +extern "C" void *reallocarray(void *pointer, usize nmembers, usize size) { + usize total_size; + if (__builtin_mul_overflow(nmembers, size, &total_size)) { + return nullptr; // errno = ENOMEM + } + + return Kernel::GlobalMemoryAllocator::the().reallocate( + reinterpret_cast(pointer), total_size, true, + __builtin_return_address(0)); } diff --git a/Kernel/GlobalMemoryAllocator.hpp b/Kernel/GlobalMemoryAllocator.hpp index 700dcee..9de342d 100644 --- a/Kernel/GlobalMemoryAllocator.hpp +++ b/Kernel/GlobalMemoryAllocator.hpp @@ -1,30 +1,32 @@ #pragma once -#include #include +#include #include #include -namespace Kernel -{ - class GlobalMemoryAllocator final - : public Singleton - , public MemoryAllocator - { - public: - u8* allocate(usize, bool debug_override = true, void *address = nullptr) override; - void deallocate(u8*, bool debug_override = true, void *address = nullptr) override; - u8* reallocate(u8*, usize, bool debug_override = true, void *address = nullptr) override; +namespace Kernel { +class GlobalMemoryAllocator final : public Singleton, + public MemoryAllocator { +public: + u8 *allocate(usize, bool debug_override = true, + void *address = nullptr) override; + void deallocate(u8 *, bool debug_override = true, + void *address = nullptr) override; + u8 *reallocate(u8 *, usize, bool debug_override = true, + void *address = nullptr) override; + + void set_mutex_enabled(bool enabled); - void set_mutex_enabled(bool enabled); + u8 *allocate_eternal(usize size); - private: - friend Singleton; - GlobalMemoryAllocator(); +private: + friend Singleton; + GlobalMemoryAllocator(); - Optional m_heap; + Optional m_heap; - Bytes allocate_heap(); - }; -} + Bytes allocate_heap(); +}; +} // namespace Kernel diff --git a/Kernel/Interrupt/UART.cpp b/Kernel/Interrupt/UART.cpp index 52a95f6..c6532d4 100644 --- a/Kernel/Interrupt/UART.cpp +++ b/Kernel/Interrupt/UART.cpp @@ -1,4 +1,5 @@ #include +#include #include #include diff --git a/Kernel/KernelMutex.hpp b/Kernel/KernelMutex.hpp index 028364f..18caa3b 100644 --- a/Kernel/KernelMutex.hpp +++ b/Kernel/KernelMutex.hpp @@ -1,108 +1,11 @@ #pragma once -#include -#include - -#include -#include +#include namespace Kernel { - // FIXME: Must not be accessed in handler mode - - // FIXME: Public interface, enforce lock guards - - // FIXME: Syncronize lock and unlock themselves - - // FIXME: Fix semantics of Thread::block - - class KernelMutex - { - public: - ~KernelMutex() - { - VERIFY(m_waiting_threads.size() == 0); - } - - // FIXME: This is a bit yanky - // This could be addressed by giving Scheduler a proper constructor and put Scheduler::loop in there - - void lock() - { - if (!m_enabled) - return; - - VERIFY(Kernel::is_executing_in_thread_mode()); - - // We only have to lock if the scheduler is initialized and if threads are already being scheduled. - - if (Scheduler::is_initialized()) { - MaskedInterruptGuard interrupt_guard; - - // We must not hold a strong reference here, otherwise, this thread could not be - // terminated without being rescheduled. Not that this should happen, but... - Thread *active_thread = Scheduler::the().get_active_thread_if_avaliable(); - - if (active_thread != nullptr) { - if (m_holding_thread.is_null()) { - m_holding_thread = *active_thread; - } else { - active_thread->set_masked_from_scheduler(true); - m_waiting_threads.enqueue(*active_thread); - Scheduler::the().trigger(); - } - } - } else { - // Since the Scheduler is not initialized, we do not have to deal with locking - } - } - - void unlock() - { - if (!m_enabled) - return; - - VERIFY(Kernel::is_executing_in_thread_mode()); - - if (Scheduler::is_initialized()) { - MaskedInterruptGuard interrupt_guard; - - m_holding_thread.clear(); - - if (m_waiting_threads.size() > 0) { - RefPtr next_thread = m_waiting_threads.dequeue(); - - FIXME_ASSERT(m_holding_thread.is_null()); - m_holding_thread = next_thread; - - m_holding_thread->wakeup(); - } - } else { - // Since the Scheduler is not initialized, we do not have to deal with locking - - VERIFY(m_holding_thread.is_null()); - } - } - - void set_enabled(bool enabled) - { - m_enabled = enabled; - } - - bool is_locked() - { - MaskedInterruptGuard interrupt_guard; - return !m_holding_thread.is_null(); - } - - private: - // During the boot procedure it's not always possible to aquire a mutex. - // Since the scheduler isn't running at that point, we can safely access the resource without a lock. - volatile bool m_enabled = true; - - RefPtr m_holding_thread; - CircularQueue, 16> m_waiting_threads; - }; + // KernelMutex is now just a SoftwareMutex + using KernelMutex = SoftwareMutex; extern KernelMutex dbgln_mutex; extern KernelMutex malloc_mutex; diff --git a/Kernel/Loader.cpp b/Kernel/Loader.cpp index 0497bec..d2a5be1 100644 --- a/Kernel/Loader.cpp +++ b/Kernel/Loader.cpp @@ -1,5 +1,5 @@ #include - +#include #include #include #include @@ -158,8 +158,8 @@ namespace Kernel Scheduler::the().get_active_thread().m_privileged = false; } - // FIXME: Free old stack?! - // FIXME: Make sure to drop the corresponding region as well + // Ensure the stack pointer is 8-byte aligned for the entry point + stack.align(8); asm volatile("msr psp, %0;" "msr control, %1;" @@ -170,7 +170,7 @@ namespace Kernel "mov r2, %6;" "bx %3;" : - : "r"(stack.top()), // FIXME: This is wrong! + : "r"(stack.top()), "r"(0b11), "r"(executable.m_writable_base), "r"(executable.m_entry), diff --git a/Kernel/PageAllocator.cpp b/Kernel/PageAllocator.cpp index 9d0e649..c35aaf3 100644 --- a/Kernel/PageAllocator.cpp +++ b/Kernel/PageAllocator.cpp @@ -1,121 +1,178 @@ -#include -#include #include +#include +#include +#define __pico_ram_start ((u8 *)0x20000000) +#define __pico_ram_end ((u8 *)(0x20000000 + 264 * 1024)) +#define __pico_boot_ram_start ((u8 *)0x20000000) +#define __pico_boot_ram_end ((u8 *)(0x20000000 + 8 * 1024)) -extern "C" u8 __pico_ram_start[]; -extern "C" u8 __pico_ram_end[]; -extern "C" u8 __pico_boot_ram_start[]; -extern "C" u8 __pico_boot_ram_end[]; - -namespace Kernel -{ - OwnedPageRange::~OwnedPageRange() - { - if (m_range.is_valid()) - PageAllocator::the().deallocate(*this); - } - - void PageAllocator::set_mutex_enabled(bool enabled) - { - page_allocator_mutex.set_enabled(enabled); - } +namespace Kernel { +OwnedPageRange::~OwnedPageRange() { + if (m_range.is_valid()) + PageAllocator::the().deallocate(*this); +} - PageAllocator::PageAllocator() - { - for (auto& block : m_blocks.span().iter()) { - block = nullptr; - } +void PageAllocator::set_mutex_enabled(bool enabled) { + page_allocator_mutex.set_enabled(enabled); +} - // My custom linker script will allocate the first 8 KiB of RAM for statup. - // The rest can be managed by this page allocator. +PageAllocator::PageAllocator() { + for (auto &block : m_blocks) + block = nullptr; + + // The RP2040 has 264KB of RAM. + // The first 256KB is contiguous in address space (0x20000000 - 0x20040000). + // The last 8KB is striped/scratch banks (0x20040000 ...). + // + // Our linker script reserves the first 8 KB (0x20000000 - 0x20002000) for + // the bootloader (stage2), vector table, and static kernel data (.data/.bss). + // + // We initialize the Buddy System with the remaining free memory. + // We explicitly push free blocks of decreasing powers of two to cover the + // range. + // + // Layout covered: + // [8KB, 16KB) -> 8KB block + // [16KB, 32KB) -> 16KB block + // [32KB, 64KB) -> 32KB block + // [64KB, 128KB) -> 64KB block + // [128KB, 256KB)-> 128KB block + // + // Total managed: 256KB - 8KB = 248KB. + // + // Note: The scratch banks (top 8KB) are currently NOT managed by + // PageAllocator. They are used for .stack sections in the linker script. + + // Helper helper to add a block + auto add_initial_block = [&](usize offset, usize size) { + m_blocks[power_of_two(size)] = + reinterpret_cast(__pico_ram_start + offset); + }; + + add_initial_block(128 * KiB, 128 * KiB); + add_initial_block(64 * KiB, 64 * KiB); + add_initial_block(32 * KiB, 32 * KiB); + add_initial_block(16 * KiB, 16 * KiB); + add_initial_block(8 * KiB, 8 * KiB); + + // Validate blocks + for (auto *block : m_blocks) { + if (!block) + continue; + + // Ensure properly initialized + block->m_next = nullptr; + + // Sanity check bounds + uptr addr = bit_cast(block); + VERIFY(addr >= bit_cast(__pico_boot_ram_end)); + VERIFY(addr < bit_cast(__pico_ram_end)); + } +} - m_blocks[power_of_two(128 * KiB)] = reinterpret_cast(__pico_ram_start + 128 * KiB); - m_blocks[power_of_two(64 * KiB)] = reinterpret_cast(__pico_ram_start + 64 * KiB); - m_blocks[power_of_two(32 * KiB)] = reinterpret_cast(__pico_ram_start + 32 * KiB); - m_blocks[power_of_two(16 * KiB)] = reinterpret_cast(__pico_ram_start + 16 * KiB); - m_blocks[power_of_two(8 * KiB)] = reinterpret_cast(__pico_ram_start + 8 * KiB); +Optional PageAllocator::allocate(usize power) { + VERIFY(is_executing_in_thread_mode()); - for (auto& block : m_blocks.span().iter()) { - if (block == nullptr) - continue; + page_allocator_mutex.lock(); + Optional range_opt = allocate_locked(power); + page_allocator_mutex.unlock(); - VERIFY(bit_cast(block) >= bit_cast(__pico_boot_ram_end)); - VERIFY(bit_cast(block) < bit_cast(__pico_ram_end)); + if (range_opt.is_valid()) { + m_allocated_pages.insert(range_opt.value()); + return OwnedPageRange{range_opt.value()}; + } else { + return {}; + } +} - // FIXME: We appear to assert when we dereference this pointer. - block->m_next = nullptr; - } - } +Optional PageAllocator::allocate_locked(usize power) { + usize size = 1 << power; - Optional PageAllocator::allocate(usize power) - { - VERIFY(is_executing_in_thread_mode()); + if (debug_page_allocator) + dbgln("[PageAllocator::allocate] power={}", power); - page_allocator_mutex.lock(); - Optional range_opt = allocate_locked(power); - page_allocator_mutex.unlock(); + ASSERT(power <= max_power); - if (range_opt.is_valid()) { - return OwnedPageRange { range_opt.value() }; - } else { - return {}; - } - } + if (m_blocks[power] != nullptr) { + uptr base = reinterpret_cast(m_blocks[power]); - Optional PageAllocator::allocate_locked(usize power) - { - usize size = 1 << power; + if (debug_page_allocator) + dbgln("[PageAllocator::allocate] Found suitable block {}", base); - if (debug_page_allocator) - dbgln("[PageAllocator::allocate] power={}", power); + m_blocks[power] = m_blocks[power]->m_next; + return PageRange{power, base}; + } - ASSERT(power <= max_power); + ASSERT(power < max_power); - if (m_blocks[power] != nullptr) { - uptr base = reinterpret_cast(m_blocks[power]); + auto block_opt = allocate_locked(power + 1); + if (!block_opt.is_valid()) { + return {}; + } + auto block = block_opt.value(); - if (debug_page_allocator) - dbgln("[PageAllocator::allocate] Found suitable block {}", base); + deallocate_locked(PageRange{power, block.m_base + size}); - m_blocks[power] = m_blocks[power]->m_next; - return PageRange { power, base }; - } + return PageRange{power, block.m_base}; +} - ASSERT(power < max_power); +void PageAllocator::deallocate(OwnedPageRange &owned_range) { + VERIFY(is_executing_in_thread_mode()); - auto block_opt = allocate_locked(power + 1); - if (!block_opt.is_valid()) { - return {}; - } - auto block = block_opt.value(); + PageRange range = owned_range.m_range.must(); + owned_range.m_range.clear(); - deallocate_locked(PageRange{ power, block.m_base + size }); + page_allocator_mutex.lock(); + m_allocated_pages.remove(range); + deallocate_locked(range); + page_allocator_mutex.unlock(); +} - return PageRange { power, block.m_base }; - } +void PageAllocator::deallocate_locked(PageRange range) { + if (debug_page_allocator) + dbgln("[PageAllocator::deallocate] power={} base={}", range.m_power, + range.m_base); - void PageAllocator::deallocate(OwnedPageRange& owned_range) - { - VERIFY(is_executing_in_thread_mode()); + ASSERT(range.m_power <= max_power); - PageRange range = owned_range.m_range.must(); - owned_range.m_range.clear(); + // Try to coalesce with buddy + if (range.m_power < max_power) { + usize size = 1 << range.m_power; + uptr buddy_addr = range.m_base ^ size; - page_allocator_mutex.lock(); - deallocate_locked(range); - page_allocator_mutex.unlock(); - } + // Check if buddy is in the free list for this power + Block *prev = nullptr; + Block *current = m_blocks[range.m_power]; - void PageAllocator::deallocate_locked(PageRange range) - { + while (current) { + if (reinterpret_cast(current) == buddy_addr) { + // Buddy is free! Coalesce. if (debug_page_allocator) - dbgln("[PageAllocator::deallocate] power={} base={}", range.m_power, range.m_base); + dbgln("[PageAllocator::deallocate] Coalescing {} with buddy {}", + range.m_base, buddy_addr); - ASSERT(range.m_power <= max_power); + // Remove buddy from list + if (prev) { + prev->m_next = current->m_next; + } else { + m_blocks[range.m_power] = current->m_next; + } - auto *block_ptr = reinterpret_cast(range.m_base); - block_ptr->m_next = m_blocks[range.m_power]; - m_blocks[range.m_power] = block_ptr; + // Recurse with combined block + // The combined block starts at the lower of the two addresses + uptr combined_base = + (range.m_base < buddy_addr) ? range.m_base : buddy_addr; + deallocate_locked(PageRange{range.m_power + 1, combined_base}); + return; + } + prev = current; + current = current->m_next; } + } + + auto *block_ptr = reinterpret_cast(range.m_base); + block_ptr->m_next = m_blocks[range.m_power]; + m_blocks[range.m_power] = block_ptr; } +} // namespace Kernel diff --git a/Kernel/PageAllocator.hpp b/Kernel/PageAllocator.hpp index b94a488..252a026 100644 --- a/Kernel/PageAllocator.hpp +++ b/Kernel/PageAllocator.hpp @@ -1,98 +1,112 @@ #pragma once -#include #include -#include #include +#include +#include #include -namespace Kernel -{ - // This must not be turned on in early boot. - // We need to allocate some memory before we are able to produce output. - volatile inline bool debug_page_allocator = false; - - struct PageRange { - usize m_power; - uptr m_base; - - const u8* data() const { return reinterpret_cast(m_base); } - u8* data() { return reinterpret_cast(m_base); } - - usize size() const { return 1 << m_power; } - - ReadonlyBytes bytes() const { return { data(), size() }; } - Bytes bytes() { return { data(), size() }; } - }; - - class OwnedPageRange { - public: - explicit OwnedPageRange(PageRange range) - : m_range(range) - { - } - OwnedPageRange(const OwnedPageRange&) = delete; - OwnedPageRange(OwnedPageRange&& other) - { - m_range = move(other.m_range); - } - ~OwnedPageRange(); - - OwnedPageRange& operator=(const OwnedPageRange&) = delete; - - OwnedPageRange& operator=(OwnedPageRange&& other) - { - m_range = move(other.m_range); - return *this; - } - - usize size() const { return m_range->size(); } - - const u8* data() const { return m_range->data(); } - u8* data() { return m_range->data(); } - - ReadonlyBytes bytes() const { return m_range->bytes(); } - Bytes bytes() { return m_range->bytes(); } - - Optional m_range; - }; - - class PageAllocator : public Singleton { - public: - static constexpr usize max_power = 19; - static constexpr usize stack_power = power_of_two(0x800); - - Optional allocate(usize power); - void deallocate(OwnedPageRange&); - - // FIXME: Syncronize - void dump() - { - dbgln("[PageAllocator] blocks:"); - for (usize power = 0; power < max_power; ++power) { - dbgln(" [{}]: {}", power, m_blocks[power]); - } - } - - void set_mutex_enabled(bool enabled); - - private: - friend Singleton; - PageAllocator(); - - Optional allocate_locked(usize power); - void deallocate_locked(PageRange); - - // There is quite a bit of trickery going on here: - // - // - The address of this block is encoded indirectly in the address of this object - // - // - The size of this block is encoded indirection in the index used to access m_blocks - struct Block { - Block *m_next; - }; - - Array m_blocks; - }; -} +#include + +namespace Kernel { +// This must not be turned on in early boot. +// We need to allocate some memory before we are able to produce output. +volatile inline bool debug_page_allocator = false; + +struct PageRange { + usize m_power; + uptr m_base; + + const u8 *data() const { return reinterpret_cast(m_base); } + u8 *data() { return reinterpret_cast(m_base); } + + usize size() const { return 1 << m_power; } + + ReadonlyBytes bytes() const { return {data(), size()}; } + Bytes bytes() { return {data(), size()}; } + + bool operator<(const PageRange &other) const { return m_base < other.m_base; } + bool operator>(const PageRange &other) const { return m_base > other.m_base; } + bool operator==(const PageRange &other) const { + return m_base == other.m_base; + } + bool operator!=(const PageRange &other) const { + return m_base != other.m_base; + } +}; + +class OwnedPageRange { +public: + explicit OwnedPageRange(PageRange range) : m_range(range) {} + OwnedPageRange(const OwnedPageRange &) = delete; + OwnedPageRange(OwnedPageRange &&other) { m_range = move(other.m_range); } + ~OwnedPageRange(); + + OwnedPageRange &operator=(const OwnedPageRange &) = delete; + + OwnedPageRange &operator=(OwnedPageRange &&other) { + m_range = move(other.m_range); + return *this; + } + + usize size() const { return m_range->size(); } + + const u8 *data() const { return m_range->data(); } + u8 *data() { return m_range->data(); } + + ReadonlyBytes bytes() const { return m_range->bytes(); } + Bytes bytes() { return m_range->bytes(); } + + Optional m_range; +}; + +class PageAllocator : public Singleton { +public: + static constexpr usize max_power = 19; + static constexpr usize stack_power = power_of_two(0x800); + + Optional allocate(usize power); + void deallocate(OwnedPageRange &); + + // FIXME: Syncronize + void dump() { + dbgln("[PageAllocator] blocks:"); + for (usize power = 0; power < max_power; ++power) { + dbgln(" [{}]: {}", power, m_blocks[power]); + } + dbgln("[PageAllocator] allocated: {}", m_allocated_pages); + } + + void set_mutex_enabled(bool enabled); + +private: + friend Singleton; + PageAllocator(); + + Optional allocate_locked(usize power); + void deallocate_locked(PageRange); + + // There is quite a bit of trickery going on here: + // + // - The address of this block is encoded indirectly in the address of this + // object + // + // - The size of this block is encoded indirection in the index used to + // access m_blocks + struct Block { + Block *m_next; + }; + + Array m_blocks; + SortedSet m_allocated_pages; +}; +} // namespace Kernel + +namespace Std { +template <> struct Formatter { + static void format(StringBuilder &builder, const Kernel::PageRange &value) { + builder.appendf("[base={x}, power={}]", value.m_base, value.m_power); + } +}; +} // namespace Std diff --git a/Kernel/Process.cpp b/Kernel/Process.cpp index c2bf392..4e38c81 100644 --- a/Kernel/Process.cpp +++ b/Kernel/Process.cpp @@ -1,5 +1,5 @@ #include - +#include #include #include #include @@ -9,6 +9,33 @@ namespace Kernel { + Process::~Process() + { + // Clean up file handles to prevent leaks + for (auto node : m_handles.iter()) { + VirtualFileHandle* handle = node.m_value.value(); + delete handle; + } + } + + i32 Process::add_file_handle(VirtualFileHandle& handle) + { + i32 handle_id = m_next_handle_id++; + m_handles.set(handle_id, &handle); + + return handle_id; + } + + void Process::close_file_handle(i32 fd) + { + auto* handle_ptr = m_handles.get(fd); + if (handle_ptr) { + VirtualFileHandle* handle = *handle_ptr; + m_handles.remove(fd); + delete handle; + } + } + Process& Process::active() { auto& thread = Scheduler::the().get_active_thread(); diff --git a/Kernel/Process.hpp b/Kernel/Process.hpp index 7c73a0d..90c628c 100644 --- a/Kernel/Process.hpp +++ b/Kernel/Process.hpp @@ -16,18 +16,16 @@ namespace Kernel i32 m_status; }; + ~Process(); + static Process& active(); static Process& create(StringView name, ElfWrapper); static Process& create(StringView name, ElfWrapper, const Vector& arguments, const Vector& variables); - i32 add_file_handle(VirtualFileHandle& handle) - { - i32 handle_id = m_next_handle_id++; - m_handles.set(handle_id, &handle); + i32 add_file_handle(VirtualFileHandle& handle); + void close_file_handle(i32 fd); - return handle_id; - } VirtualFileHandle& get_file_handle(i32 fd) { diff --git a/Kernel/StackProtector.cpp b/Kernel/StackProtector.cpp new file mode 100644 index 0000000..f3d8000 --- /dev/null +++ b/Kernel/StackProtector.cpp @@ -0,0 +1,52 @@ +#include +#include + +extern "C" [[noreturn]] void panic(const char *fmt, ...); + +// Random value for the stack canary. +// Stack canary initialization is handled by init_stack_guard() +#include +#include +#include + +extern "C" { +// Random value for the stack canary. +// Initialized to a magic value, then localized by init_stack_guard() +uintptr_t __stack_chk_guard = 0xDEADBEEF; + +[[noreturn]] void __stack_chk_fail() { + panic("Stack Smash Detected! (Canary corrupted)"); +} +} + +namespace Kernel { +void init_stack_guard() { + // Use the ROSC (Ring Oscillator) RANDOMBIT register to generate a random + // canary This is a simple hardware random source on the RP2040. + + volatile uint32_t *rosc_randombit = + (volatile uint32_t *)(ROSC_BASE + ROSC_RANDOMBIT_OFFSET); + uintptr_t random_canary = 0; + + for (size_t i = 0; i < sizeof(uintptr_t) * 8; ++i) { + random_canary = (random_canary << 1) | (*rosc_randombit & 1); + // Delay slightly to allow ROSC to fluctuate? + // In practice, the loop overhead is enough. + } + + // Ensure canary has null byte to terminate string overflows? + // Common SSP tactic: 0x000aff0d (null, newline, etc). + // For now, full random is fine, but let's ensure the lowest byte is non-zero + // to avoid accidental null termination issues if we want to print it? + // Actually, preventing string functions from reading PAST the canary is + // better if the canary contains a nullptr. Linux uses 0 in the first byte for + // 64-bit, logic is: strcpy stops at canary. + + // Let's set the LSB to 0 to stop string functions. + random_canary &= ~0xFF; + + __stack_chk_guard = random_canary; + + dbgln("Stack Protection Initialized. Canary: {x}", __stack_chk_guard); +} +} // namespace Kernel diff --git a/Kernel/StackWrapper.hpp b/Kernel/StackWrapper.hpp index 45cb810..c5524a2 100644 --- a/Kernel/StackWrapper.hpp +++ b/Kernel/StackWrapper.hpp @@ -1,65 +1,68 @@ #pragma once -#include - #include +#include -namespace Kernel -{ - struct StackWrapper { - explicit StackWrapper(Bytes bytes) - : m_bytes(bytes) - , m_top(bytes.data() + bytes.size()) - { - } - - u8* reserve(usize count) - { - ASSERT(m_bytes.data() + count <= m_top); - return m_top -= count; - } +namespace Kernel { +// A helper class to manage pushing data onto a downward-growing stack. +// Useful for setting up thread stacks (arguments, environment, initial +// context). +struct StackWrapper { + explicit StackWrapper(Bytes bytes) + : m_bytes(bytes), + m_top(bytes.data() + bytes.size()) // Start at the end (high address) + {} - u8* push(ReadonlyBytes bytes) - { - u8 *data = reserve(bytes.size()); - bytes.copy_to({ data, bytes.size() }); - return data; - } + // Reserve space on the stack by moving the top pointer down. + // Returns the new pointer to the reserved space. + u8 *reserve(usize count) { + ASSERT(m_bytes.data() + count <= m_top); + return m_top -= count; + } - template - T* push_value(const T& value) - { - u8 *data = reserve(sizeof(value)); - return new (data) T { value }; - } + // Push a raw byte buffer onto the stack. + u8 *push(ReadonlyBytes bytes) { + u8 *data = reserve(bytes.size()); + bytes.copy_to({data, bytes.size()}); + return data; + } - char* push_cstring(const char *cstring) - { - reserve(__builtin_strlen(cstring) + 1); + // Push a POD value onto the stack. + template T *push_value(const T &value) { + u8 *data = reserve(sizeof(value)); + return new (data) T{value}; + } - // FIXME: Is this necessary? - char *cstring_on_stack = (char*)align(4); + // Push a null-terminated string onto the stack. + // Ensures the string pointer is aligned to 4 bytes for AAPCS compliance / + // efficiency. + char *push_cstring(const char *cstring) { + usize length = __builtin_strlen(cstring) + 1; // +1 for null terminator + reserve(length); - __builtin_strcpy(cstring_on_stack, cstring); - return cstring_on_stack; - } + // Align the pointer *downwards* to specific boundary (usually 4 for ARM + // strings) This introduces padding *above* the string if necessary. + char *cstring_on_stack = (char *)align(4); - u8* align(u32 boundary) - { - static_assert(sizeof(u8*) == sizeof(u32)); - if (u32(m_top) % boundary != 0) - reserve(u32(m_top) % boundary); + __builtin_strcpy(cstring_on_stack, cstring); + return cstring_on_stack; + } - return m_top; - } + // Align the stack pointer downwards to the given boundary. + // Boundary must be a power of two. + u8 *align(u32 boundary) { + static_assert(sizeof(u8 *) == sizeof(u32)); + uptr address = (uptr)m_top; + if (address % boundary != 0) { + reserve(address % boundary); + } + return m_top; + } - u8* top() - { - return m_top; - } + u8 *top() const { return m_top; } - private: - Bytes m_bytes; - u8 *m_top; - }; -} +private: + Bytes m_bytes; + u8 *m_top; +}; +} // namespace Kernel diff --git a/Kernel/Synchronization/HardwareSpinLock.hpp b/Kernel/Synchronization/HardwareSpinLock.hpp index faea98e..37f966d 100644 --- a/Kernel/Synchronization/HardwareSpinLock.hpp +++ b/Kernel/Synchronization/HardwareSpinLock.hpp @@ -26,12 +26,21 @@ namespace Kernel MaskedInterruptGuard interrupt_guard; while (true) { - u32 value; - __atomic_load(m_spin_lock_pointer, &value, __ATOMIC_SEQ_CST); - - // We read non-zero, if the lock was aquired sucessfully. - if (value != 0) - continue; + // Reading from the spin lock register attempts to claim it. + // A non-zero value means we successfully claimed the lock (value is the lock bitmap). + // A zero value means the lock was already claimed by someone else. + u32 value = *m_spin_lock_pointer; + + if (value != 0) { + // We got the lock! + // Full memory barrier to ensure subsequent operations verify against the lock acquisition. + __sync_synchronize(); + return; + } + + // We failed to get the lock. + // Wait for an event (SEV) from the other core to save power before retrying. + asm volatile("wfe"); } } @@ -40,9 +49,14 @@ namespace Kernel // We only want to synchronize with the other core, not with other threads. MaskedInterruptGuard interrupt_guard; + // Full memory barrier to ensure all operations finishing before we release the lock. + __sync_synchronize(); + // Writing anything will release the lock. - u32 value = 1; - __atomic_store(m_spin_lock_pointer, &value, __ATOMIC_SEQ_CST); + *m_spin_lock_pointer = 1; + + // Signal the other core that the lock is free. + asm volatile("sev"); } }; } diff --git a/Kernel/Synchronization/MaskedInterruptGuard.hpp b/Kernel/Synchronization/MaskedInterruptGuard.hpp index ca6ab28..5eeff50 100644 --- a/Kernel/Synchronization/MaskedInterruptGuard.hpp +++ b/Kernel/Synchronization/MaskedInterruptGuard.hpp @@ -1,4 +1,5 @@ #pragma once +#include namespace Kernel { diff --git a/Kernel/Synchronization/ScopedLock.hpp b/Kernel/Synchronization/ScopedLock.hpp new file mode 100644 index 0000000..64aa439 --- /dev/null +++ b/Kernel/Synchronization/ScopedLock.hpp @@ -0,0 +1,23 @@ +#pragma once + +#include + +namespace Kernel { +template class ScopedLock { +public: + explicit ScopedLock(LockType &lock) : m_lock(lock) { m_lock.lock(); } + + ~ScopedLock() { m_lock.unlock(); } + + // Disable copy/move + ScopedLock(const ScopedLock &) = delete; + ScopedLock &operator=(const ScopedLock &) = delete; + ScopedLock(ScopedLock &&) = delete; + ScopedLock &operator=(ScopedLock &&) = delete; + +private: + LockType &m_lock; +}; + +using ScopedMutex = ScopedLock; +} // namespace Kernel diff --git a/Kernel/Synchronization/SoftwareMutex.cpp b/Kernel/Synchronization/SoftwareMutex.cpp new file mode 100644 index 0000000..4f49011 --- /dev/null +++ b/Kernel/Synchronization/SoftwareMutex.cpp @@ -0,0 +1,85 @@ +#include +#include +#include + +namespace Kernel +{ + void SoftwareMutex::initialize(HardwareSpinLock& lock) + { + m_lock = &lock; + } + + void SoftwareMutex::lock() + { + if (!Scheduler::is_initialized()) + return; + + // Passive lock: must be called with interrupts enabled (logic requirement), + // but we assume thread context. + VERIFY(is_executing_in_thread_mode()); + + MaskedInterruptGuard interrupt_guard; + ASSERT(m_lock); + + m_lock->lock(); + + if (m_owner.is_null()) { + m_owner = Scheduler::the().get_active_thread(); + m_lock->unlock(); + } else { + auto& thread = Scheduler::the().get_active_thread(); + thread.set_masked_from_scheduler(true); + m_waiting_threads.enqueue(thread); + + m_lock->unlock(); + + Scheduler::the().trigger(); // Yield + + // Re-aquired implicitely by unlock() passing ownership? + // If unlock sets m_owner = next, we are good. + } + } + + void SoftwareMutex::unlock() + { + if (!Scheduler::is_initialized()) { + m_owner.clear(); + return; + } + + VERIFY(is_executing_in_thread_mode()); + MaskedInterruptGuard interrupt_guard; + ASSERT(m_lock); + + m_lock->lock(); + + // Verify we own it? + // ASSERT(m_owner == Scheduler::the().get_active_thread()); + + if (m_waiting_threads.size() == 0) { + m_owner.clear(); + } else { + auto next_thread = m_waiting_threads.dequeue(); + m_owner = next_thread; + next_thread->wakeup(); + } + + m_lock->unlock(); + } + + bool SoftwareMutex::is_locked() + { + if (!Scheduler::is_initialized()) + return false; + + MaskedInterruptGuard interrupt_guard; + ASSERT(m_lock); + + // We can peek without lock if we just want a hint, but for correctness: + m_lock->lock(); + bool locked = !m_owner.is_null(); + m_lock->unlock(); + + return locked; + } +} diff --git a/Kernel/Synchronization/SoftwareMutex.hpp b/Kernel/Synchronization/SoftwareMutex.hpp new file mode 100644 index 0000000..fa1b35c --- /dev/null +++ b/Kernel/Synchronization/SoftwareMutex.hpp @@ -0,0 +1,30 @@ +#pragma once + +#include +#include +#include +#include +#include + +namespace Kernel +{ + class SoftwareMutex final : public AbstractLock + { + public: + SoftwareMutex() = default; + + void initialize(HardwareSpinLock& lock); + + virtual void lock() override; + virtual void unlock() override; + + // API Compatibility for KernelMutex + void set_enabled(bool enabled) { (void)enabled; } + bool is_locked(); + + private: + HardwareSpinLock* m_lock = nullptr; + CircularQueue, 16> m_waiting_threads; + RefPtr m_owner; + }; +} diff --git a/Kernel/Synchronization/SoftwareSpinLock.cpp b/Kernel/Synchronization/SoftwareSpinLock.cpp new file mode 100644 index 0000000..1261d18 --- /dev/null +++ b/Kernel/Synchronization/SoftwareSpinLock.cpp @@ -0,0 +1,68 @@ +#include +#include +#include + +namespace Kernel +{ + HardwareSpinLock* SoftwareSpinLock::s_hardware_lock = nullptr; + + void SoftwareSpinLock::initialize(HardwareSpinLock& hardware_lock) + { + s_hardware_lock = &hardware_lock; + } + + static inline u32 get_core_num() + { + return sio_hw->cpuid; + } + + void SoftwareSpinLock::lock() + { + // Must be used with interrupts disabled to prevent deadlocks with ISRs on the same core + ASSERT(!are_interrupts_enabled()); + ASSERT(s_hardware_lock != nullptr); + + u32 current_core = get_core_num(); + + // Deadlock detection: Check if we already own the lock + if (m_owning_core == (i32)current_core) { + // Panic or crash + ASSERT(false && "Deadlock detected: SoftwareSpinLock already held by this core!"); + } + + while (true) { + s_hardware_lock->lock(); + + if (!m_locked) { + m_locked = true; + m_owning_core = (i32)current_core; + s_hardware_lock->unlock(); + return; + } + + s_hardware_lock->unlock(); + + // Wait for Event (sleep until SEV from unlock) + asm volatile("wfe"); + } + } + + void SoftwareSpinLock::unlock() + { + ASSERT(!are_interrupts_enabled()); + ASSERT(s_hardware_lock != nullptr); + + s_hardware_lock->lock(); + + ASSERT(m_locked); + ASSERT(m_owning_core == (i32)get_core_num()); + + m_locked = false; + m_owning_core = -1; + + s_hardware_lock->unlock(); + + // Signal other cores that the lock is free + asm volatile("sev"); + } +} diff --git a/Kernel/Synchronization/SoftwareSpinLock.hpp b/Kernel/Synchronization/SoftwareSpinLock.hpp new file mode 100644 index 0000000..0638261 --- /dev/null +++ b/Kernel/Synchronization/SoftwareSpinLock.hpp @@ -0,0 +1,24 @@ +#pragma once + +#include +#include +#include + +namespace Kernel +{ + class SoftwareSpinLock final : public AbstractLock + { + public: + SoftwareSpinLock() = default; + + virtual void lock() override; + virtual void unlock() override; + + static void initialize(HardwareSpinLock& hardware_lock); + + private: + volatile bool m_locked = false; + volatile i32 m_owning_core = -1; + static HardwareSpinLock* s_hardware_lock; + }; +} diff --git a/Kernel/Synchronization/WaitingThreadQueue.cpp b/Kernel/Synchronization/WaitingThreadQueue.cpp new file mode 100644 index 0000000..cef59d5 --- /dev/null +++ b/Kernel/Synchronization/WaitingThreadQueue.cpp @@ -0,0 +1,47 @@ +#include +#include + +namespace Kernel +{ + void WaitingThreadQueue::initialize(HardwareSpinLock& lock) + { + m_lock = &lock; + } + + void WaitingThreadQueue::enqueue(Thread& thread) + { + ASSERT(!are_interrupts_enabled()); + ASSERT(m_lock); + + m_lock->lock(); + m_threads.enqueue(RefPtr(thread)); + m_lock->unlock(); + } + + RefPtr WaitingThreadQueue::dequeue() + { + ASSERT(!are_interrupts_enabled()); + ASSERT(m_lock); + + m_lock->lock(); + auto thread = m_threads.dequeue(); + m_lock->unlock(); + return thread; + } + + bool WaitingThreadQueue::is_empty() const + { + // Note: access without lock is racy if interrupts/other cores involved, + // but typically used after lock held or for heuristic. + // For correctness, we should lock. But 'const' makes it hard with the current lock signature? + // Let's rely on caller or remove const/add mutable to lock. + // Actually HardwareSpinLock::lock() is not const. + // Implementation note: Ideally we lock here too. + return m_threads.size() == 0; + } + + usize WaitingThreadQueue::size() const + { + return m_threads.size(); + } +} diff --git a/Kernel/Synchronization/WaitingThreadQueue.hpp b/Kernel/Synchronization/WaitingThreadQueue.hpp new file mode 100644 index 0000000..6c2dd17 --- /dev/null +++ b/Kernel/Synchronization/WaitingThreadQueue.hpp @@ -0,0 +1,27 @@ +#pragma once + +#include +#include +#include +#include + +namespace Kernel +{ + class WaitingThreadQueue + { + public: + WaitingThreadQueue() = default; + + // Must be called to assign the lock that protects this queue + void initialize(HardwareSpinLock& lock); + + void enqueue(Thread& thread); + RefPtr dequeue(); + bool is_empty() const; + usize size() const; + + private: + HardwareSpinLock* m_lock = nullptr; + CircularQueue, 32> m_threads; + }; +} diff --git a/Kernel/SystemHandler.cpp b/Kernel/SystemHandler.cpp index ebfc6b6..01da504 100644 --- a/Kernel/SystemHandler.cpp +++ b/Kernel/SystemHandler.cpp @@ -1,5 +1,5 @@ #include - +#include #include #include #include diff --git a/Kernel/Threads/Scheduler.cpp b/Kernel/Threads/Scheduler.cpp index cc81bf8..9cd0487 100644 --- a/Kernel/Threads/Scheduler.cpp +++ b/Kernel/Threads/Scheduler.cpp @@ -1,233 +1,249 @@ -#include -#include -#include #include +#include #include +#include +#include +#include #include #include -namespace Kernel -{ - extern "C" - { - FullRegisterContext& scheduler_next(FullRegisterContext& context) - { - if (debug_scheduler) - dbgln("[scheduler_next]"); - - Thread& active_thread = Scheduler::the().get_active_thread(); - active_thread.stash_context(context); - - Thread& next_thread = Scheduler::the().schedule(); - return next_thread.unstash_context(); - } - - void isr_systick() - { - if (Scheduler::the().m_enabled) - scb_hw->icsr = M0PLUS_ICSR_PENDSVSET_BITS; - } - } +namespace Kernel { +extern "C" { +FullRegisterContext &scheduler_next(FullRegisterContext &context) { + if (debug_scheduler) + dbgln("[scheduler_next]"); - Scheduler::Scheduler(RefPtr startup_thread) - { - if (scheduler_slow) - systick_hw->rvr = 0x00f00000; - else - systick_hw->rvr = 0x000f0000; + Thread &active_thread = Scheduler::the().get_active_thread(); + active_thread.stash_context(context); - systick_hw->csr = 1 << M0PLUS_SYST_CSR_CLKSOURCE_LSB - | 1 << M0PLUS_SYST_CSR_TICKINT_LSB - | 1 << M0PLUS_SYST_CSR_ENABLE_LSB; + Thread &next_thread = Scheduler::the().schedule(); + return next_thread.unstash_context(); +} - m_queued_threads.enqueue(move(startup_thread)); - } +void isr_systick() { + if (Scheduler::the().m_enabled) + scb_hw->icsr = M0PLUS_ICSR_PENDSVSET_BITS; +} +} - Thread& Scheduler::schedule() - { - VERIFY(is_executing_in_handler_mode()); - - // First, we need to save the previous active thread somehow. - if (m_active_thread.is_null()) { - // There are situations where we do not have an active thread. - // This can happen when we are in a system call for example. - } else { - if (m_active_thread->m_masked_from_scheduler) { - // We are not allowed to drop the last reference here, because we are in handler mode. - // If this is the last reference, then we need to clean it up elsewhere. - if (m_active_thread->refcount() >= 2) { - m_active_thread.clear(); - } else { - m_dangling_threads.enqueue(move(m_active_thread)); - } - } else if (m_active_thread->m_is_default_thread) { - // The default thread should not be queued. - VERIFY(m_active_thread->refcount() >= 2); - m_active_thread.clear(); - } else { - // Schedule this thread again at a later point. - m_queued_threads.enqueue(move(m_active_thread)); - } - } - - // Next, we need to choose a new thread to schedule. - VERIFY(m_active_thread.is_null()); - if (m_dangling_threads.size() >= 1) { - // We need to drop the reference to a dangling thread. - m_active_thread = choose_default_thread(); - } else if (m_queued_threads.size() == 0) { - // We have no normal threads that should be scheduled. - m_active_thread = choose_default_thread(); - } else { - m_active_thread = m_queued_threads.dequeue(); - } - - // Setup control register for privileged/unprivileged execution. - if (m_active_thread->m_privileged) { - asm volatile("msr control, %0;" - "isb;" - : - : "r"(0b10)); - } else { - asm volatile("msr control, %0;" - "isb;" - : - : "r"(0b11)); - } - - // Setup the memory protection unit. - // Since we are in an interrupt handler, this will only apply after we return. - setup_mpu(m_active_thread->m_regions); - - return m_active_thread.must(); - } +Scheduler::Scheduler(RefPtr startup_thread, HardwareSpinLock &lock) { + m_lock = &lock; - void Scheduler::trigger() - { - VERIFY(m_enabled); - VERIFY(is_executing_in_thread_mode()); + if (scheduler_slow) + systick_hw->rvr = 0x00f00000; + else + systick_hw->rvr = 0x000f0000; - scb_hw->icsr = M0PLUS_ICSR_PENDSVSET_BITS; - } + systick_hw->csr = 1 << M0PLUS_SYST_CSR_CLKSOURCE_LSB | + 1 << M0PLUS_SYST_CSR_TICKINT_LSB | + 1 << M0PLUS_SYST_CSR_ENABLE_LSB; - void Scheduler::loop() - { - // This is a special thread that will be scheduled, if nothing else can be scheduled. - m_default_thread = Thread::construct("Kernel: Default Thread"); - m_default_thread->m_privileged = true; - m_default_thread->m_is_default_thread = true; - - m_default_thread->setup_context([&] { - for (;;) { - if (debug_scheduler) - dbgln("[Scheduler] Running default thread. (refcount={})", m_default_thread->refcount()); - - MaskedInterruptGuard interrupt_guard; - - if (m_dangling_threads.size() == 0) { - // There is nothing sensible we can do, give the scheduler another opportunity. - Scheduler::the().trigger(); - continue; - } - - RefPtr thread = m_dangling_threads.dequeue(); - - // At this point, we no longer need to synchronize, the cleanup can happen in parallel. - interrupt_guard.release(); - - // I do not know, if this can happen, better check for it. - VERIFY(!dbgln_mutex.is_locked()); - - if (debug_scheduler) - dbgln("[Scheduler] We are about to kill thread '{}' (refcount={})", thread->m_name, thread->refcount()); - - // Remove the last reference to this thread and thus kill it. - VERIFY(thread->refcount() == 1); - thread.clear(); - } - }); - - // This is a special thread that will be scheduled if the default thread is blocking. - // That can happen when it is trying to create debug output. - m_fallback_thread = Thread::construct("Kernel: Fallback Thread"); - m_fallback_thread->m_privileged = true; - m_fallback_thread->m_is_default_thread = true; - - m_fallback_thread->setup_context([] { - for (;;) { - // Give the scheduler another chance. - Scheduler::the().trigger(); - } - }); - - // This is a special thread that should die immediately. - // We simply need some way of entering the scheduler. - auto dummy_thread = Thread::construct("Dummy"); - dummy_thread->m_masked_from_scheduler = true; - - dummy_thread->setup_context([] { - dbgln("[Scheduler] Dummy thread is running."); - - // From now on, we need to be careful with synchronization. - GlobalMemoryAllocator::the().set_mutex_enabled(true); - PageAllocator::the().set_mutex_enabled(true); - - Scheduler::the().m_enabled = true; - Scheduler::the().trigger(); - VERIFY_NOT_REACHED(); - }); - - { - MaskedInterruptGuard interrupt_guard; - m_active_thread = dummy_thread; - } - - FullRegisterContext& context = dummy_thread->unstash_context(); - - u32 control = 0b10; - asm volatile("msr control, %0;" - "isb;" - "mov r0, %1;" - "blx restore_context_from_thread_mode;" - : - : "r"(control), "r"(&context) - : "r0"); - - VERIFY_NOT_REACHED(); - } + m_queued_threads.enqueue(move(startup_thread)); +} - void Scheduler::dump() - { - VERIFY(is_executing_in_handler_mode() || !are_interrupts_enabled()); - - dbgln("[Scheduler] m_queued_threads:"); - for (size_t i = 0; i < m_queued_threads.size(); ++i) { - Thread& thread = *m_queued_threads[i]; - dbgln(" {} @{}", thread.m_name, &thread); - } - - dbgln("[Scheduler] m_danging_threads:"); - for (size_t i = 0; i < m_dangling_threads.size(); ++i) { - Thread& thread = *m_dangling_threads[i]; - dbgln(" {} @{}", thread.m_name, &thread); - } - - dbgln("[Scheduler] m_default_thread:"); - { - Thread& thread = m_default_thread.must(); - dbgln(" {} @{}", thread.m_name, &thread); - } +Thread &Scheduler::schedule() { + VERIFY(is_executing_in_handler_mode()); + u32 cpuid = sio_hw->cpuid; + + m_lock->lock(); + + // First, we need to save the previous active thread somehow. + auto &active_thread = m_active_threads[cpuid]; + + if (active_thread.is_null()) { + // There are situations where we do not have an active thread. + // This can happen when we are in a system call for example. + } else { + if (active_thread->m_masked_from_scheduler) { + // We are not allowed to drop the last reference here, because we are in + // handler mode. If this is the last reference, then we need to clean it + // up elsewhere. + if (active_thread->refcount() >= 2) { + active_thread.clear(); + } else { + m_dangling_threads.enqueue(move(active_thread)); + } + } else if (active_thread->m_is_default_thread) { + // The default thread should not be queued. + VERIFY(active_thread->refcount() >= 2); + active_thread.clear(); + } else { + // Schedule this thread again at a later point. + m_queued_threads.enqueue(move(active_thread)); } + } + + // Next, we need to choose a new thread to schedule. + VERIFY(active_thread.is_null()); + if (m_dangling_threads.size() >= 1) { + // We need to drop the reference to a dangling thread. + active_thread = choose_default_thread(); + } else if (m_queued_threads.size() == 0) { + // We have no normal threads that should be scheduled. + active_thread = choose_default_thread(); + } else { + active_thread = m_queued_threads.dequeue(); + } + + m_lock->unlock(); + + // Setup control register for privileged/unprivileged execution. + if (active_thread->m_privileged) { + asm volatile("msr control, %0;" + "isb;" + : + : "r"(0b10)); + } else { + asm volatile("msr control, %0;" + "isb;" + : + : "r"(0b11)); + } + + // Setup the memory protection unit. + // Since we are in an interrupt handler, this will only apply after we return. + setup_mpu(active_thread->m_regions); + + return active_thread.must(); +} + +void Scheduler::trigger() { + VERIFY(m_enabled); + VERIFY(is_executing_in_thread_mode()); - RefPtr Scheduler::choose_default_thread() - { - if (m_default_thread->m_masked_from_scheduler) { - VERIFY(!m_fallback_thread->m_masked_from_scheduler); - return m_fallback_thread; - } else { - return m_default_thread; - } + scb_hw->icsr = M0PLUS_ICSR_PENDSVSET_BITS; +} + +void Scheduler::loop() { + u32 cpuid = sio_hw->cpuid; + + // This is a special thread that will be scheduled, if nothing else can be + // scheduled. + m_default_threads[cpuid] = Thread::construct("Kernel: Default Thread"); + auto &default_thread = m_default_threads[cpuid]; + default_thread->m_privileged = true; + default_thread->m_is_default_thread = true; + + default_thread->setup_context([&] { + for (;;) { + if (debug_scheduler) + dbgln("[Scheduler] Running default thread. (refcount={})", + m_default_threads[sio_hw->cpuid]->refcount()); + + MaskedInterruptGuard interrupt_guard; + + // Lock to check dangling threads + m_lock->lock(); + if (m_dangling_threads.size() == 0) { + m_lock->unlock(); + // There is nothing sensible we can do, give the scheduler another + // opportunity. + Scheduler::the().trigger(); + continue; + } + + RefPtr thread = m_dangling_threads.dequeue(); + m_lock->unlock(); + + // At this point, we no longer need to synchronize, the cleanup can happen + // in parallel. + interrupt_guard.release_early(); + + // I do not know, if this can happen, better check for it. + VERIFY(!dbgln_mutex.is_locked()); + + if (debug_scheduler) + dbgln("[Scheduler] We are about to kill thread '{}' (refcount={})", + thread->m_name, thread->refcount()); + + // Remove the last reference to this thread and thus kill it. + VERIFY(thread->refcount() == 1); + thread.clear(); + } + }); + + // This is a special thread that will be scheduled if the default thread is + // blocking. That can happen when it is trying to create debug output. + m_fallback_threads[cpuid] = Thread::construct("Kernel: Fallback Thread"); + auto &fallback_thread = m_fallback_threads[cpuid]; + fallback_thread->m_privileged = true; + fallback_thread->m_is_default_thread = true; + + fallback_thread->setup_context([] { + for (;;) { + // Give the scheduler another chance. + Scheduler::the().trigger(); } + }); + + // This is a special thread that should die immediately. + // We simply need some way of entering the scheduler. + auto dummy_thread = Thread::construct("Dummy"); + dummy_thread->m_masked_from_scheduler = true; + + dummy_thread->setup_context([] { + dbgln("[Scheduler] Dummy thread is running."); + + // From now on, we need to be careful with synchronization. + GlobalMemoryAllocator::the().set_mutex_enabled(true); + PageAllocator::the().set_mutex_enabled(true); + + Scheduler::the().m_enabled = true; + Scheduler::the().trigger(); + VERIFY_NOT_REACHED(); + }); + + { + MaskedInterruptGuard interrupt_guard; + m_lock->lock(); + m_active_threads[cpuid] = dummy_thread; + m_lock->unlock(); + } + + FullRegisterContext &context = dummy_thread->unstash_context(); + + u32 control = 0b10; + asm volatile("msr control, %0;" + "isb;" + "mov r0, %1;" + "blx restore_context_from_thread_mode;" + : + : "r"(control), "r"(&context) + : "r0"); + + VERIFY_NOT_REACHED(); +} + +void Scheduler::dump() { + VERIFY(is_executing_in_handler_mode() || !are_interrupts_enabled()); + + dbgln("[Scheduler] m_queued_threads:"); + for (size_t i = 0; i < m_queued_threads.size(); ++i) { + Thread &thread = *m_queued_threads[i]; + dbgln(" {} @{}", thread.m_name, &thread); + } + + dbgln("[Scheduler] m_dangling_threads:"); + for (size_t i = 0; i < m_dangling_threads.size(); ++i) { + Thread &thread = *m_dangling_threads[i]; + dbgln(" {} @{}", thread.m_name, &thread); + } + + dbgln("[Scheduler] m_default_threads:"); + for (size_t i = 0; i < 2; ++i) { + Thread &thread = *m_default_threads[i]; + dbgln(" {} @{}", thread.m_name, &thread); + } +} +RefPtr Scheduler::choose_default_thread() { + u32 cpuid = sio_hw->cpuid; + if (m_default_threads[cpuid]->m_masked_from_scheduler) { + VERIFY(!m_fallback_threads[cpuid]->m_masked_from_scheduler); + return m_fallback_threads[cpuid]; + } else { + return m_default_threads[cpuid]; + } } +} // namespace Kernel diff --git a/Kernel/Threads/Scheduler.hpp b/Kernel/Threads/Scheduler.hpp index fbdecab..2a4cba7 100644 --- a/Kernel/Threads/Scheduler.hpp +++ b/Kernel/Threads/Scheduler.hpp @@ -1,70 +1,71 @@ #pragma once +#include #include #include -#include #include -#include -#include -#include #include +#include +#include + +#include +#include + +namespace Kernel { +constexpr bool debug_scheduler = false; +constexpr bool scheduler_slow = false; + +class Scheduler : public Singleton { +public: + Thread *get_active_thread_if_avaliable() { + VERIFY(is_executing_in_handler_mode() || !are_interrupts_enabled()); + return m_active_threads[sio_hw->cpuid]; + } + + Thread &get_active_thread() { + VERIFY(is_executing_in_handler_mode() || !are_interrupts_enabled()); + auto &thread = m_active_threads[sio_hw->cpuid]; + VERIFY(thread != nullptr); + return *thread; + } + + void clear_active_thread() { + VERIFY(is_executing_in_handler_mode() || !are_interrupts_enabled()); + auto &thread = m_active_threads[sio_hw->cpuid]; + VERIFY(!thread.is_null()); + thread.clear(); + } + + Thread &schedule(); + + void add_thread(RefPtr thread) { + VERIFY(is_executing_in_handler_mode() || !are_interrupts_enabled()); + m_queued_threads.enqueue(thread); + } + + void dump(); + + void loop(); + void trigger(); + + bool m_enabled = false; + + // In thread mode, we must disable interrupts to interact with these. + // For multi-thread support, we should add a mutex here. + CircularQueue, 16> m_queued_threads; + CircularQueue, 16> m_dangling_threads; + Array, 2> m_active_threads; + + HardwareSpinLock *m_lock = nullptr; + +private: + Array, 2> m_default_threads; + Array, 2> m_fallback_threads; + + friend Singleton; + Scheduler(RefPtr startup_thread, HardwareSpinLock &lock); -namespace Kernel -{ - constexpr bool debug_scheduler = false; - constexpr bool scheduler_slow = false; - - class Scheduler : public Singleton { - public: - Thread* get_active_thread_if_avaliable() - { - VERIFY(is_executing_in_handler_mode() || !are_interrupts_enabled()); - return m_active_thread; - } - - Thread& get_active_thread() - { - VERIFY(is_executing_in_handler_mode() || !are_interrupts_enabled()); - VERIFY(m_active_thread != nullptr); - return *m_active_thread; - } - - void clear_active_thread() - { - VERIFY(is_executing_in_handler_mode() || !are_interrupts_enabled()); - VERIFY(!m_active_thread.is_null()); - m_active_thread.clear(); - } - - Thread& schedule(); - - void add_thread(RefPtr thread) - { - VERIFY(is_executing_in_handler_mode() || !are_interrupts_enabled()); - m_queued_threads.enqueue(thread); - } - - void dump(); - - void loop(); - void trigger(); - - bool m_enabled = false; - - // In thread mode, we must disable interrupts to interact with these. - // For multi-thread support, we should add a mutex here. - CircularQueue, 16> m_queued_threads; - CircularQueue, 16> m_dangling_threads; - RefPtr m_active_thread = nullptr; - - private: - RefPtr m_default_thread; - RefPtr m_fallback_thread; - - friend Singleton; - Scheduler(RefPtr startup_thread); - - RefPtr choose_default_thread(); - }; -} + RefPtr choose_default_thread(); +}; +} // namespace Kernel diff --git a/Kernel/Threads/Thread.cpp b/Kernel/Threads/Thread.cpp index 500b6d6..fcc92ed 100644 --- a/Kernel/Threads/Thread.cpp +++ b/Kernel/Threads/Thread.cpp @@ -215,8 +215,7 @@ namespace Kernel if (debug_syscall) dbgln("Thread::sys$close"); - // FIXME - + m_process->close_file_handle(fd); return 0; } diff --git a/Kernel/Threads/Thread.hpp b/Kernel/Threads/Thread.hpp index 220c631..63eba30 100644 --- a/Kernel/Threads/Thread.hpp +++ b/Kernel/Threads/Thread.hpp @@ -71,9 +71,13 @@ namespace Kernel }; using CallbackContainer = decltype(callback_container); - u8 *callback_container_on_stack = stack_wrapper.reserve(sizeof(CallbackContainer)); + // Ensure 8-byte alignment for the callback container to satisfy AAPCS and prevent unaligned access faults + stack_wrapper.align(8); + usize container_size = sizeof(CallbackContainer); + if (container_size % 8 != 0) + container_size += 8 - (container_size % 8); - // FIXME: We hardfault right here, it seems that 'callback_container_on_stack' makes no sense + u8 *callback_container_on_stack = stack_wrapper.reserve(container_size); new (callback_container_on_stack) CallbackContainer { move(callback_container) }; void (*callback_container_wrapper)(void*) = type_erased_member_function_wrapper; diff --git a/Kernel/cpu.S b/Kernel/cpu.S index 445393f..448b4e4 100644 --- a/Kernel/cpu.S +++ b/Kernel/cpu.S @@ -89,26 +89,58 @@ isr_svcall: .global restore_context_from_thread_mode .thumb_func restore_context_from_thread_mode: - bl pop_callee_saved_registers + // r0 points to the FullRegisterContext (stack top) - mov sp, r0 + // Switch to PSP to use the thread's stack + msr psp, r0 + movs r1, #2 // CONTROL.SPSEL = 1 (PSP) + msr control, r1 + isb - // FIXME: This is quite a mess, try to simplify? + // Load High Registers & Status from Stack (Peek) + ldr r1, [sp, #28] // xPSR + msr apsr_nzcvq, r1 - ldr r0, [sp, #0x10] - mov ip, r0 + ldr r1, [sp, #20] // LR + mov lr, r1 - ldr r0, [sp, #0x14] - mov lr, r0 + ldr r1, [sp, #16] // R12 + mov ip, r1 - ldr r0, [sp, #0x1c] - msr apsr_nzcvq, r0 - isb + // Restore Low Registers + pop {r0, r1, r2, r3} - // FIXME: Do pop/str modify APSR? + // Skip R12 (4) and LR (4) - we already loaded them + add sp, #8 - pop {r0, r1, r2, r3} - str r0, [sp, #0] - str r1, [sp, #4] - pop {r0, r1} + // Pop PC to jump to the thread entry point + // Note: This leaves xPSR (4 bytes) on the stack, effectively leaking 4 bytes. + // This is acceptable as this function is only used for the initial thread launch. pop {pc} + +.global isr_hardfault +.thumb_func +isr_hardfault: + mov r0, lr + movs r1, #4 + tst r0, r1 + beq kernel_hard_fault + + // User mode fault (PSP) + mrs r0, psp + isb + bl push_callee_saved_registers + bl hard_fault_handler_cpp + bl pop_callee_saved_registers + msr psp, r0 + isb + ldr r0, =0xfffffffd + bx r0 + +kernel_hard_fault: + // Kernel mode fault (MSP) + // We pass the stack pointer to the handler + mrs r0, msp + mov r1, lr + bl kernel_hard_fault_handler_cpp + b . diff --git a/Kernel/linker.ld b/Kernel/linker.ld new file mode 100644 index 0000000..b5b7dca --- /dev/null +++ b/Kernel/linker.ld @@ -0,0 +1,289 @@ +/* Based on GCC ARM embedded samples. + Defines the following symbols for use by code: + __exidx_start + __exidx_end + __etext + __data_start__ + __preinit_array_start + __preinit_array_end + __init_array_start + __init_array_end + __fini_array_start + __fini_array_end + __data_end__ + __bss_start__ + __bss_end__ + __end__ + end + __HeapLimit + __StackLimit + __StackTop + __stack (== StackTop) +*/ + +MEMORY +{ + INCLUDE "pico_flash_region.ld" + RAM(rwx) : ORIGIN = 0x20000000, LENGTH = 256k + SCRATCH_X(rwx) : ORIGIN = 0x20040000, LENGTH = 4k + SCRATCH_Y(rwx) : ORIGIN = 0x20041000, LENGTH = 4k +} + +ENTRY(_entry_point) + +SECTIONS +{ + /* Second stage bootloader is prepended to the image. It must be 256 bytes big + and checksummed. It is usually built by the boot_stage2 target + in the Raspberry Pi Pico SDK + */ + + .flash_begin : { + __flash_binary_start = .; + } > FLASH + + .boot2 : { + __boot2_start__ = .; + KEEP (*(.boot2)) + __boot2_end__ = .; + } > FLASH + + ASSERT(__boot2_end__ - __boot2_start__ == 256, + "ERROR: Pico second stage bootloader must be 256 bytes in size") + + /* The second stage will always enter the image at the start of .text. + The debugger will use the ELF entry point, which is the _entry_point + symbol if present, otherwise defaults to start of .text. + This can be used to transfer control back to the bootrom on debugger + launches only, to perform proper flash setup. + */ + + .text : { + __logical_binary_start = .; + KEEP (*(.vectors)) + KEEP (*(.binary_info_header)) + __binary_info_header_end = .; + KEEP (*(.embedded_block)) + __embedded_block_end = .; + KEEP (*(.reset)) + /* memset/memcpy/float are excluded from flash to run from RAM for speed/safety */ + /* bit of a hack right now to exclude all floating point and time critical (e.g. memset, memcpy) code from + * FLASH ... we will include any thing excluded here in .data below by default */ + *(.init) + *(EXCLUDE_FILE(*libgcc.a: *libc.a:*lib_a-mem*.o *libm.a:) .text*) + *(.fini) + /* Pull all c'tors into .text */ + *crtbegin.o(.ctors) + *crtbegin?.o(.ctors) + *(EXCLUDE_FILE(*crtend?.o *crtend.o) .ctors) + *(SORT(.ctors.*)) + *(.ctors) + /* Followed by destructors */ + *crtbegin.o(.dtors) + *crtbegin?.o(.dtors) + *(EXCLUDE_FILE(*crtend?.o *crtend.o) .dtors) + *(SORT(.dtors.*)) + *(.dtors) + + . = ALIGN(4); + /* preinit data */ + PROVIDE_HIDDEN (__preinit_array_start = .); + KEEP(*(SORT(.preinit_array.*))) + KEEP(*(.preinit_array)) + PROVIDE_HIDDEN (__preinit_array_end = .); + + . = ALIGN(4); + /* init data */ + PROVIDE_HIDDEN (__init_array_start = .); + KEEP(*(SORT(.init_array.*))) + KEEP(*(.init_array)) + PROVIDE_HIDDEN (__init_array_end = .); + + . = ALIGN(4); + /* finit data */ + PROVIDE_HIDDEN (__fini_array_start = .); + *(SORT(.fini_array.*)) + *(.fini_array) + PROVIDE_HIDDEN (__fini_array_end = .); + + *(.eh_frame*) + . = ALIGN(4); + } > FLASH + + .rodata : { + *(EXCLUDE_FILE(*libgcc.a: *libc.a:*lib_a-mem*.o *libm.a:) .rodata*) + . = ALIGN(4); + *(SORT_BY_ALIGNMENT(SORT_BY_NAME(.flashdata*))) + . = ALIGN(4); + } > FLASH + + .ARM.extab : + { + *(.ARM.extab* .gnu.linkonce.armextab.*) + } > FLASH + + __exidx_start = .; + .ARM.exidx : + { + *(.ARM.exidx* .gnu.linkonce.armexidx.*) + } > FLASH + __exidx_end = .; + + /* Machine inspectable binary information */ + . = ALIGN(4); + __binary_info_start = .; + .binary_info : + { + KEEP(*(.binary_info.keep.*)) + *(.binary_info.*) + } > FLASH + __binary_info_end = .; + . = ALIGN(4); + + .ram_vector_table (NOLOAD): { + *(.ram_vector_table) + } > RAM + + .uninitialized_data (NOLOAD): { + . = ALIGN(4); + *(.uninitialized_data*) + } > RAM + + .data : { + __data_start__ = .; + *(vtable) + + *(.time_critical*) + + /* remaining .text and .rodata; i.e. stuff we exclude above because we want it in RAM */ + *(.text*) + . = ALIGN(4); + *(.rodata*) + . = ALIGN(4); + + *(.data*) + + . = ALIGN(4); + *(.after_data.*) + . = ALIGN(4); + /* preinit data */ + PROVIDE_HIDDEN (__mutex_array_start = .); + KEEP(*(SORT(.mutex_array.*))) + KEEP(*(.mutex_array)) + PROVIDE_HIDDEN (__mutex_array_end = .); + + . = ALIGN(4); + *(.jcr) + . = ALIGN(4); + } > RAM AT> FLASH + + .tdata : { + . = ALIGN(4); + *(.tdata .tdata.* .gnu.linkonce.td.*) + /* All data end */ + __tdata_end = .; + } > RAM AT> FLASH + PROVIDE(__data_end__ = .); + + /* __etext is (for backwards compatibility) the name of the .data init source pointer (...) */ + __etext = LOADADDR(.data); + + .tbss (NOLOAD) : { + . = ALIGN(4); + __bss_start__ = .; + __tls_base = .; + *(.tbss .tbss.* .gnu.linkonce.tb.*) + *(.tcommon) + + __tls_end = .; + } > RAM + + .bss (NOLOAD) : { + . = ALIGN(4); + __tbss_end = .; + + *(SORT_BY_ALIGNMENT(SORT_BY_NAME(.bss*))) + *(COMMON) + . = ALIGN(8); /* Ensure 8-byte alignment for .heap start */ + __bss_end__ = .; + } > RAM + + .heap (NOLOAD) : ALIGN(8) + { + __end__ = .; + end = __end__; + KEEP(*(.heap*)) + . = ALIGN(8); + } > RAM + /* historically on GCC sbrk was growing past __HeapLimit to __StackLimit, however + to be more compatible, we now set __HeapLimit explicitly to where the end of the heap is */ + __HeapLimit = ORIGIN(RAM) + LENGTH(RAM); + + /* Start and end symbols must be word-aligned */ + .scratch_x : { + __scratch_x_start__ = .; + *(.scratch_x.*) + . = ALIGN(4); + __scratch_x_end__ = .; + } > SCRATCH_X AT > FLASH + __scratch_x_source__ = LOADADDR(.scratch_x); + + .scratch_y : { + __scratch_y_start__ = .; + *(.scratch_y.*) + . = ALIGN(4); + __scratch_y_end__ = .; + } > SCRATCH_Y AT > FLASH + __scratch_y_source__ = LOADADDR(.scratch_y); + + /* .stack*_dummy section doesn't contains any symbols. It is only + * used for linker to calculate size of stack sections, and assign + * values to stack symbols later + * + * stack1 section may be empty/missing if platform_launch_core1 is not used */ + + /* by default we put core 0 stack at the end of scratch Y, so that if core 1 + * stack is not used then all of SCRATCH_X is free. + */ + .stack1_dummy (NOLOAD): + { + *(.stack1*) + } > SCRATCH_X + .stack_dummy (NOLOAD): + { + KEEP(*(.stack*)) + } > SCRATCH_Y + + .flash_end : { + KEEP(*(.embedded_end_block*)) + PROVIDE(__flash_binary_end = .); + } > FLASH + + /* stack limit is poorly named, but historically is maximum heap ptr */ + __StackLimit = ORIGIN(RAM) + LENGTH(RAM); + __StackOneTop = ORIGIN(SCRATCH_X) + LENGTH(SCRATCH_X); + __StackTop = ORIGIN(SCRATCH_Y) + LENGTH(SCRATCH_Y); + + /* Ensure stack bottoms are 8-byte aligned */ + __StackOneBottom = (__StackOneTop - SIZEOF(.stack1_dummy)) & ~7; + __StackBottom = (__StackTop - SIZEOF(.stack_dummy)) & ~7; + + PROVIDE(__stack = __StackTop); + + /* picolibc and LLVM */ + PROVIDE (__heap_start = __end__); + PROVIDE (__heap_end = __HeapLimit); + PROVIDE( __tls_align = MAX(ALIGNOF(.tdata), ALIGNOF(.tbss)) ); + PROVIDE( __tls_size_align = (__tls_size + __tls_align - 1) & ~(__tls_align - 1)); + PROVIDE( __arm32_tls_tcb_offset = MAX(8, __tls_align) ); + + /* llvm-libc */ + PROVIDE (_end = __end__); + PROVIDE (__llvm_libc_heap_limit = __HeapLimit); + + /* Check if data + heap + stack exceeds RAM limit */ + ASSERT(__StackLimit >= __HeapLimit, "region RAM overflowed") + + ASSERT( __binary_info_header_end - __logical_binary_start <= 256, "Binary info must be in first 256 bytes of the binary") + /* todo assert on extra code */ +} diff --git a/Kernel/main.cpp b/Kernel/main.cpp index 45a9d6d..fb053a9 100644 --- a/Kernel/main.cpp +++ b/Kernel/main.cpp @@ -1,18 +1,20 @@ -#include +#include +#include +#include #include +#include -#include #include -#include -#include #include -#include +#include +#include #include -#include -#include #include +#include #include +#include #include +#include #include @@ -23,65 +25,75 @@ extern "C" u8 __end__[]; extern "C" u8 __HeapLimit[]; -namespace Kernel -{ - // FIXME: Clean this up - void create_shell_process() - { - auto& shell_file = dynamic_cast(Kernel::FileSystem::lookup("/bin/Shell.elf")); - Kernel::ElfWrapper elf { shell_file.m_data.data(), "Userland/Shell.1.elf" }; - Kernel::Process::create("/bin/Shell.elf", move(elf)); - } +namespace Kernel { +// FIXME: Clean this up +void create_shell_process() { + auto &shell_file = dynamic_cast( + Kernel::FileSystem::lookup("/bin/Shell.elf")); + Kernel::ElfWrapper elf{shell_file.m_data.data(), "Userland/Shell.1.elf"}; + Kernel::Process::create("/bin/Shell.elf", move(elf)); +} - void boot_with_scheduler(); +void boot_with_scheduler(); - // Setup basic systems and run 'boot_with_scheduler' in a new thread - void boot() - { - Kernel::PageAllocator::initialize(); - Kernel::PageAllocator::the().set_mutex_enabled(false); +// Hardware Spin Locks (RP2040 SIO Base 0xd0000000 + 0x100 offset) +static HardwareSpinLock + s_sw_spinlock_hw_lock((volatile u32 *)0xd0000100); // Lock 0 +static HardwareSpinLock s_malloc_hw_lock((volatile u32 *)0xd0000104); // Lock 1 +static HardwareSpinLock s_page_hw_lock((volatile u32 *)0xd0000108); // Lock 2 +static HardwareSpinLock s_dbgln_hw_lock((volatile u32 *)0xd000010c); // Lock 3 +static HardwareSpinLock + s_scheduler_hw_lock((volatile u32 *)0xd0000110); // Lock 4 - Kernel::GlobalMemoryAllocator::initialize(); - Kernel::GlobalMemoryAllocator::the().set_mutex_enabled(false); +// Setup basic systems and run 'boot_with_scheduler' in a new thread +void boot() { + // Initialize Synchronization Primitives + SoftwareSpinLock::initialize(s_sw_spinlock_hw_lock); + malloc_mutex.initialize(s_malloc_hw_lock); + page_allocator_mutex.initialize(s_page_hw_lock); + dbgln_mutex.initialize(s_dbgln_hw_lock); - Kernel::Interrupt::UART::initialize(); - Kernel::ConsoleFile::initialize(); + Kernel::PageAllocator::initialize(); + Kernel::PageAllocator::the().set_mutex_enabled(false); - dbgln("\e[0;1mBOOT\e[0m"); + Kernel::GlobalMemoryAllocator::initialize(); + Kernel::GlobalMemoryAllocator::the().set_mutex_enabled(false); - auto thread = Kernel::Thread::construct("Kernel (boot_with_scheduler)"); - thread->setup_context(boot_with_scheduler); - thread->m_privileged = true; + Kernel::Interrupt::UART::initialize(); + Kernel::ConsoleFile::initialize(); - Kernel::Scheduler::initialize(move(thread)); - Kernel::Scheduler::the().loop(); - } + dbgln("\e[0;1mBOOT\e[0m"); - void boot_with_scheduler() - { - Kernel::FlashFileSystem::initialize(); - Kernel::MemoryFileSystem::initialize(); - Kernel::DeviceFileSystem::initialize(); + auto thread = Kernel::Thread::construct("Kernel (boot_with_scheduler)"); + thread->setup_context(boot_with_scheduler); + thread->m_privileged = true; - dbgln("__HeapLimit={} __end__={}", __HeapLimit, __end__); + Kernel::Scheduler::initialize(move(thread), s_scheduler_hw_lock); + Kernel::Scheduler::the().loop(); +} - dbgln("[main] Creating /example.txt"); - auto& example_file = *new Kernel::MemoryFile; - auto& example_handle = example_file.create_handle(); - example_handle.write({ (const u8*)"Hello, world!\n", 14 }); +void boot_with_scheduler() { + Kernel::FlashFileSystem::initialize(); + Kernel::MemoryFileSystem::initialize(); + Kernel::DeviceFileSystem::initialize(); - auto& root_file = Kernel::FileSystem::lookup("/"); - dynamic_cast(root_file).m_entries.set("example.txt", &example_file); + dbgln("__HeapLimit={} __end__={}", __HeapLimit, __end__); - Kernel::SystemHandler::initialize(); + dbgln("[main] Creating /example.txt"); + auto &example_file = *new Kernel::MemoryFile; + auto &example_handle = example_file.create_handle(); + example_handle.write({(const u8 *)"Hello, world!\n", 14}); - // debug_page_allocator = true; + auto &root_file = Kernel::FileSystem::lookup("/"); + dynamic_cast(root_file).m_entries.set( + "example.txt", &example_file); - create_shell_process(); - } -} + Kernel::SystemHandler::initialize(); -int main() -{ - Kernel::boot(); + // debug_page_allocator = true; + + create_shell_process(); } +} // namespace Kernel + +int main() { Kernel::boot(); } diff --git a/README.md b/README.md index d977fbf..f2dee48 100644 --- a/README.md +++ b/README.md @@ -1,108 +1,92 @@ -# PicoOS - -This is a simple operating system for the Raspberry Pi Pico micro-controller. -The code structure is strongly inspired by SerenityOS, however, no code is taken from that source directly. - -My goal was to write a simple operating system because I was interested in operating systems. -The code base is a huge mess and I later tried to rewrite everything, however, I never completed the rewrite. - -![Screen Capture of Terminal Connected to System](Docs/demo.gif) - -Currently the system has the following capabilities: - -- The system runs on a Raspberry Pi Pico microcontroller (actual hardware!). - It uses a UART connection to communicate with a terminal window of the host machine. - - Instead of connecting the device directly, it is connected indirectly with another Raspberry Pi Pico which has the PicoProbe - software instealled. - This makes it possible to debug what is happening on the chip and the UART connection is exposed via USB. - -- There is a very fragile file system implementation that supports most common operations. - Some programs are embedded into the flash memory of the device and they are accessible in this file system. - -- The system itself has an extremely simple shell program which is loaded on startup and which is accessible with the UART connection. - - There are some builtin shell commands, but it can also use `posix_spawn` to start a new process. - This can be used to load any ELF file, but the system makes a ton of assumptions about the application. - -The kernel has the following capabilities: - -- There is a bare bone memory allocation algorithm. - -- There is a scheduler that must run on a single core. - It can switch between several threads that belong either to the kernel or to userland. - - I tried adding multi-core support later on, however, the debugging tools I was using were not sophisticated enough to debug what went wrong. - -- There is some bare bone isolation between kernel and userland. - - Sadly, this microcontroller does not have a Memory Management Unit (MMU), therefore, proper isolation isn't possible. - However, the Memory Protection Unit (MPU) is used to at least prevent accesses to kernel space. - -- The following system calls are partially supported: `read`, `write`, `open`, `close`, `fstat`, `wait`, `exit`, - `chdir`, `get_working_directory`, `posix_spawn`. - - Notice, that `fork` is not on that list since it requires an MMU, however, `posix_spawn` can do most of the things that `fork` can do. - -### Development Environment +
- 1. Install required packages: - - ```none - pacman -S --needed python-invoke arm-none-eabi-gcc arm-none-eabi-gdb arm-none-eabi-newlib fmt - ``` - - 2. Install TIO from AUR: - - ```none - cdm ~/src/aur.archlinux.org - git clone --depth 1 https://aur.archlinux.org/tio.git - cd tio - makepkg --install - ``` - - 3. Build `openocd`: +# PicoOS - ```none - cdm ~/src/github.com/raspberrypi - git clone --branch picoprobe --depth 1 git@github.com:raspberrypi/openocd.git - cd openocd - ./bootstrap - CFLAGS=-Wno-error ./configure --enable-picoprobe - make -j24 - sudo make install +**PicoOS** is a simple, microkernel-inspired operating system designed for the Raspberry Pi Pico (RP2040) microcontroller. It features a custom kernel, a virtual file system, multi-core locking primitives, and a basic shell. + +![Screen Capture](Docs/images/demo.gif) + +
+ +## Documentation + +Full documentation is available in the [Docs/](Docs/index.md) directory. + +* [**Getting Started**](Docs/getting_started/index.md): Installation, build instructions, and flashing. +* [**Architecture**](Docs/architecture/system_overview.md): System design, kernel internals, and synchronization. +* [**Reference**](Docs/reference/syscalls.md): System calls and API usage. + +## ✨ Features + +* **Platform**: Runs natively on RP2040 (Raspberry Pi Pico). +* **Kernel**: + * Round-robin Scheduler. + * Wait-state efficient Synchronization (`SoftwareSpinLock`, `SoftwareMutex`). + * Virtual File System (VFS) with `/dev` and Flash support. + * MPU-based memory protection (Supervisor/User isolation). + * **Security**: Stack Smashing Protection (SSP) with Hardware RNG (ROSC). + * **Memory**: Dynamic Kernel Heap, Buddy System Page Allocator. +* **Userland**: + * ELF Executable loading (`posix_spawn`). + * Basic Shell and Editor. + * Standard C Library (partial implementation). + +## 🚀 Quick Start + +### 1. Prerequisites + +You will need the following tools: +* **Toolchain**: `arm-none-eabi-gcc`, `newlib` +* **Build System**: `cmake`, `ninja` +* **Debug**: `gdb-multiarch` (or `arm-none-eabi-gdb`), `openocd` +* **Utilities**: `python3`, `tio` (serial terminal) + +#### Ubuntu / Debian +```bash +sudo apt update +sudo apt install build-essential cmake ninja-build python3-invoke \ + gcc-arm-none-eabi libnewlib-arm-none-eabi libstdc++-arm-none-eabi-newlib \ + gdb-multiarch libfmt-dev \ + automake autoconf texinfo libtool libftdi-dev libusb-1.0-0-dev # For OpenOCD +``` + +#### Arch Linux +```bash +sudo pacman -S --needed python-invoke arm-none-eabi-gcc arm-none-eabi-gdb \ + arm-none-eabi-newlib fmt ninja cmake openocd +``` +*Note: For `tio`, install from AUR (e.g., `yay -S tio`).* + +### 2. Build OpenOCD (Ubuntu/Debian) +Arch Linux users can skip this if they installed the `openocd` package. + +```bash +git clone https://github.com/raspberrypi/openocd.git --branch rpi-common --depth=1 +cd openocd +git submodule update --init --recursive +./bootstrap +./configure --enable-picoprobe --enable-internal-jimtcl +make -j$(nproc) +sudo make install +``` + +### 3. Build PicoOS +```bash +mkdir build && cd build +# PICO_SDK_FETCH_FROM_GIT=ON automatically downloads the SDK +cmake .. -G Ninja -DPICO_SDK_FETCH_FROM_GIT=ON +ninja +``` +This generates `build/Kernel.1.uf2`. + +### 4. Flash and Connect +1. **Flash**: Hold BOOTSEL on Pico, connect USB, and run: + ```bash + inv flash ``` - - 4. Build `pico-sdk`: - - ```none - cdm ~/dev - git clone --branch tweaks git@github.com:asynts/pico-sdk.git +2. **Connect**: + ```bash + inv tty ``` - 4. Build the project with: - - ```none - cdm Build - cmake .. -GNinja -DPICO_SDK_PATH=~/dev/pico-sdk - ninja - ``` - - 4. Connect Raspberry Pi Pico. The scripts expect two Raspberry devices where - one is used for debugging and the other runs the operating system. There - needs to be a UART connection from the debugee to the debugger. - - The debugger runs the picoprobe firmware. - - 5. Run `inv probe` to start up `openocd`. - - 6. Run `inv tty`, this will be the shell into the target system. - - 7. Run `inv dbg`, this will be used for debugging and to load the application. - -### Running the System - - 1. In the debugger terminal, run `rebuild`. - - 2. `run` will start the system. The shell is accessible in the `inv tty` - terminal. +See [Getting Started](Docs/getting_started/index.md) for full details. diff --git a/Std/Array.hpp b/Std/Array.hpp index 7358541..35cde65 100644 --- a/Std/Array.hpp +++ b/Std/Array.hpp @@ -4,15 +4,38 @@ #include namespace Std { - template - class Array { - public: - T __array[Size]; - - const T& operator[](usize index) const { return __array[index]; } - T& operator[](usize index) { return __array[index]; } - - Span span() const { return { __array, Size }; } - Span span() { return { __array, Size }; } - }; -} +template class Array { +public: + T __array[Size]; + + [[nodiscard]] constexpr usize size() const { return Size; } + [[nodiscard]] constexpr T *data() { return __array; } + [[nodiscard]] constexpr const T *data() const { return __array; } + + [[nodiscard]] constexpr T &operator[](usize index) { + // Note: Use at() for bounds checking + return __array[index]; + } + + [[nodiscard]] constexpr const T &operator[](usize index) const { + return __array[index]; + } + + [[nodiscard]] constexpr T &at(usize index) { + ASSERT(index < Size); + return __array[index]; + } + [[nodiscard]] constexpr const T &at(usize index) const { + ASSERT(index < Size); + return __array[index]; + } + + T *begin() { return __array; } + T *end() { return __array + Size; } + const T *begin() const { return __array; } + const T *end() const { return __array + Size; } + + Span span() const { return {__array, Size}; } + Span span() { return {__array, Size}; } +}; +} // namespace Std diff --git a/Std/Format.cpp b/Std/Format.cpp index 7af4f9c..2fa3e25 100644 --- a/Std/Format.cpp +++ b/Std/Format.cpp @@ -1,98 +1,93 @@ -#include #include -#include +#include #include +#include #if defined(TEST) -# include +#include #elif defined(KERNEL) -# include -# include -# include +#include +#include +#include #else -# error "Only TEST and KERNEL are supported" +#error "Only TEST and KERNEL are supported" #endif -namespace Std -{ - void dbgln_raw(StringView str) - { - if (Kernel::is_executing_in_handler_mode()) - return; +namespace Std { +void dbgln_raw(StringView str) { +#ifdef KERNEL + if (Kernel::is_executing_in_handler_mode()) + return; +#endif #ifdef KERNEL - // FIXME: For multi-core support, we will need a mutex here. - // We would need to mask interrupts and then get the mutex. - // However, that will be quite involved, because of the deadlock risk. - { - Kernel::MaskedInterruptGuard interrupt_guard; + // FIXME: For multi-core support, we will need a mutex here. + // We would need to mask interrupts and then get the mutex. + // However, that will be quite involved, because of the deadlock risk. + { + Kernel::MaskedInterruptGuard interrupt_guard; - StringView prefix = "\e[36m"; - StringView suffix = "\e[0m\n"; + StringView prefix = "\e[36m"; + StringView suffix = "\e[0m\n"; - Kernel::ConsoleFileHandle handle; - handle.write(prefix.bytes()); - handle.write(str.bytes()); - handle.write(suffix.bytes()); - } + Kernel::ConsoleFileHandle handle(Kernel::ConsoleFile::the()); + handle.write(prefix.bytes()); + handle.write(str.bytes()); + handle.write(suffix.bytes()); + } #else - std::cout << "\e[36m" << std::string_view { str.data(), str.size() } << "\e[0m\n"; + std::cout << "\e[36m" << std::string_view{str.data(), str.size()} + << "\e[0m\n"; #endif - } - - template - requires Concepts::Integral - void Formatter::format(StringBuilder& builder, T value) - { - if (value < 0) { - builder.append('-'); - return format(builder, -value); - } - - builder.append("0x"); +} - char buffer[sizeof(T) * 2]; - for (usize index = 0; index < sizeof(buffer); ++index) { - buffer[index] = "0123456789abcdef"[value % 16]; - value /= 16; - } +template + requires Concepts::Integral +void Formatter::format(StringBuilder &builder, T value) { + if (value < 0) { + builder.append('-'); + return format(builder, -value); + } - for (usize index = 0; index < sizeof(buffer); ++index) - builder.append(buffer[(sizeof(buffer) - 1) - index]); - } + builder.append("0x"); - void Formatter::format(StringBuilder& builder, StringView value) - { - builder.append(value); - return; - } + char buffer[sizeof(T) * 2]; + for (usize index = 0; index < sizeof(buffer); ++index) { + buffer[index] = "0123456789abcdef"[value % 16]; + value /= 16; + } - void Formatter::format(StringBuilder& builder, bool value) - { - if (value) - builder.append("true"); - else - builder.append("false"); - } - void Formatter::format(StringBuilder& builder, char value) - { - builder.append(value); - } + for (usize index = 0; index < sizeof(buffer); ++index) + builder.append(buffer[(sizeof(buffer) - 1) - index]); +} - void Formatter::format(StringBuilder& builder, const Path& value) - { - builder.append(value.string()); - } +void Formatter::format(StringBuilder &builder, StringView value) { + builder.append(value); + return; +} +void Formatter::format(StringBuilder &builder, bool value) { + if (value) + builder.append("true"); + else + builder.append("false"); +} +void Formatter::format(StringBuilder &builder, char value) { + builder.append(value); +} - template class Formatter; - template class Formatter; - template class Formatter; - template class Formatter; - template class Formatter; - template class Formatter; - template class Formatter; - template class Formatter; - template class Formatter; - template class Formatter; +void Formatter::format(StringBuilder &builder, const Path &value) { + builder.append(value.string()); } + +template class Formatter; +template class Formatter; +template class Formatter; +template class Formatter; +template class Formatter; +template class Formatter; +template class Formatter; +template class Formatter; +template class Formatter; +template class Formatter; +} // namespace Std diff --git a/Std/Forward.cpp b/Std/Forward.cpp index 4c128b0..198798a 100644 --- a/Std/Forward.cpp +++ b/Std/Forward.cpp @@ -1,88 +1,85 @@ -#include #include +#include #include #if !defined(TEST) && !defined(KERNEL) -# error "Only KERNEL and TEST are supported" +#error "Only KERNEL and TEST are supported" #endif #if defined(TEST) -# include -# include +#include +#include #elif defined(KERNEL) -# include -# include +#include +#include #endif -namespace Std -{ - static void write_output(StringView value) - { +namespace Std { +static void write_output(StringView value) { #if defined(TEST) - std::cerr << std::string_view { value.data(), value.size() }; + std::cerr << std::string_view{value.data(), value.size()}; #elif defined(KERNEL) - Kernel::ConsoleFileHandle handle; + Kernel::ConsoleFileHandle handle(Kernel::ConsoleFile::the()); - // We do not aquire a mutex here, because the system may not work at - // this point. - bool were_interrupts_enabled = Kernel::disable_interrupts(); - handle.write(value.bytes()); - Kernel::restore_interrupts(were_interrupts_enabled); + // We do not aquire a mutex here, because the system may not work at + // this point. + bool were_interrupts_enabled = Kernel::disable_interrupts(); + handle.write(value.bytes()); + Kernel::restore_interrupts(were_interrupts_enabled); #endif - } - static void write_output(usize value) - { +} +static void write_output(usize value) { #if defined(TEST) - std::cerr << value; + std::cerr << value; #elif defined(KERNEL) - Kernel::ConsoleFileHandle handle; + Kernel::ConsoleFileHandle handle(Kernel::ConsoleFile::the()); - StringBuilder builder; - builder.appendf("{}", value); + StringBuilder builder; + builder.appendf("{}", value); - // We do not aquire a mutex here, because the system may not work at - // this point. - bool were_interrupts_enabled = Kernel::disable_interrupts(); - handle.write(builder.bytes()); - Kernel::restore_interrupts(were_interrupts_enabled); + // We do not aquire a mutex here, because the system may not work at + // this point. + bool were_interrupts_enabled = Kernel::disable_interrupts(); + handle.write(builder.bytes()); + Kernel::restore_interrupts(were_interrupts_enabled); #endif - } +} - void crash(const char *format, const char *condition, const char *file, usize line) - { - Std::Lexer lexer { format }; +void crash(const char *format, const char *condition, const char *file, + usize line) { + Std::Lexer lexer{format}; - while (!lexer.eof()) { - if (lexer.try_consume("%condition")) { - write_output(condition); - continue; - } + while (!lexer.eof()) { + if (lexer.try_consume("%condition")) { + write_output(condition); + continue; + } - if (lexer.try_consume("%file")) { - write_output(file); - continue; - } + if (lexer.try_consume("%file")) { + write_output(file); + continue; + } - if (lexer.try_consume("%line")) { - write_output(line); - continue; - } + if (lexer.try_consume("%line")) { + write_output(line); + continue; + } - write_output(lexer.consume_until('%')); + write_output(lexer.consume_until('%')); - if (lexer.peek_or_null() != '%') - ASSERT(lexer.eof()); - } - write_output("\n"); + if (lexer.peek_or_null() != '%') + ASSERT(lexer.eof()); + } + write_output("\n"); #if defined(TEST) - std::abort(); + std::abort(); #else - Kernel::disable_interrupts(); - asm volatile("bkpt #0;"); + Kernel::disable_interrupts(); + asm volatile("bkpt #0;"); - for(;;) - asm volatile("wfi"); + for (;;) + asm volatile("wfi"); #endif - } } +} // namespace Std diff --git a/Std/Forward.hpp b/Std/Forward.hpp index c7ae05c..2657cb6 100644 --- a/Std/Forward.hpp +++ b/Std/Forward.hpp @@ -63,12 +63,15 @@ constexpr void swap(T& lhs, T& rhs) } extern "C" -inline void strlcpy(char *destination, const char *source, usize size) noexcept +inline usize strlcpy(char *destination, const char *source, usize size) noexcept { + usize len = __builtin_strlen(source); if (size >= 1) { - __builtin_strncpy(destination, source, size - 1); - destination[size - 1] = 0; + usize n = (len < size - 1) ? len : (size - 1); + __builtin_memcpy(destination, source, n); + destination[n] = 0; } + return len; } extern "C" void* memcpy(void *destination, const void *source, usize count) noexcept; diff --git a/Std/MemoryAllocator.cpp b/Std/MemoryAllocator.cpp index b8ed0a0..31ac781 100644 --- a/Std/MemoryAllocator.cpp +++ b/Std/MemoryAllocator.cpp @@ -1,137 +1,142 @@ -#include #include +#include -namespace Std -{ - static usize round_to_word(usize size) - { - if (size % 4 != 0) - size = size + 4 - size % 4; - return size; - } +namespace Std { +static usize round_to_word(usize size) { + if (size % 4 != 0) + size = size + 4 - size % 4; + return size; +} + +MemoryAllocator::MemoryAllocator(Bytes heap) : m_heap(heap) { + m_freelist = reinterpret_cast(heap.data()); + m_freelist->m_size = heap.size() - sizeof(Node); + m_freelist->m_next = nullptr; +} + +u8 *MemoryAllocator::allocate(usize size, bool debug_override, void *address) { + if (address == nullptr) + address = __builtin_return_address(0); + + Node *previous = nullptr; + for (Node *entry = m_freelist; entry; entry = entry->m_next) { + if (entry->m_size >= size + sizeof(Node)) { + auto *next = + reinterpret_cast(entry->m_data + round_to_word(size)); + next->m_next = entry->m_next; + next->m_size = entry->m_size - round_to_word(size) - sizeof(Node); + + if (previous) + previous->m_next = next; + else + m_freelist = next; + + entry->m_next = nullptr; + entry->m_size = round_to_word(size); + + VERIFY(usize(entry->m_data) % 4 == 0); + + if (m_debug && debug_override) + dbgln("\e[32mMTRACE: @ {} + {} {}\e[0m", address, entry->m_data, size); - MemoryAllocator::MemoryAllocator(Bytes heap) - : m_heap(heap) - { - m_freelist = reinterpret_cast(heap.data()); - m_freelist->m_size = heap.size() - sizeof(Node); - m_freelist->m_next = nullptr; + return entry->m_data; } - u8* MemoryAllocator::allocate(usize size, bool debug_override, void *address) - { - if (address == nullptr) - address = __builtin_return_address(0); + previous = entry; + } - Node *previous = nullptr; - for (Node *entry = m_freelist; entry; entry = entry->m_next) - { - if (entry->m_size >= size + sizeof(Node)) - { - auto *next = reinterpret_cast(entry->m_data + round_to_word(size)); - next->m_next = entry->m_next; - next->m_size = entry->m_size - round_to_word(size) - sizeof(Node); + // Allocation failed: return nullptr so caller can handle it (e.g. expand + // heap) + return nullptr; +} - if (previous) - previous->m_next = next; - else - m_freelist = next; +void MemoryAllocator::add_memory(Bytes bytes) { + if (bytes.size() <= sizeof(Node)) + return; - entry->m_next = nullptr; - entry->m_size = round_to_word(size); + // Construct a free node at the start of the new memory + Node *node = reinterpret_cast(bytes.data()); + node->m_size = bytes.size() - sizeof(Node); - VERIFY(usize(entry->m_data) % 4 == 0); + // Use deallocate to insert/merge it into the freelist + deallocate(node->m_data, false, __builtin_return_address(0)); +} - if (m_debug && debug_override) - dbgln("\e[32mMTRACE: @ {} + {} {}\e[0m", address, entry->m_data, size); +void MemoryAllocator::deallocate(u8 *pointer, bool debug_override, + void *address) { + if (pointer == nullptr) + return; - return entry->m_data; - } + if (address == nullptr) + address = __builtin_return_address(0); - previous = entry; - } + if (m_debug && debug_override) + dbgln("\e[32mMTRACE: @ {} - {}", address, pointer); - dump(); + auto *target_entry = reinterpret_cast((u8 *)pointer - sizeof(Node)); - VERIFY_NOT_REACHED(); - } + Node *previous = nullptr; + for (auto *entry = m_freelist; entry; entry = entry->m_next) { + if (entry > target_entry) { + // Try to merge on the left + if (previous && + previous->m_data + previous->m_size == (u8 *)target_entry) { + previous->m_size += sizeof(Node) + target_entry->m_size; - void MemoryAllocator::deallocate(u8 *pointer, bool debug_override, void *address) - { - if (pointer == nullptr) - return; - - if (address == nullptr) - address = __builtin_return_address(0); - - if (m_debug && debug_override) - dbgln("\e[32mMTRACE: @ {} - {}", address, pointer); - - auto *target_entry = reinterpret_cast((u8*)pointer - sizeof(Node)); - - Node *previous = nullptr; - for (auto *entry = m_freelist; entry; entry = entry->m_next) - { - if (entry > target_entry) - { - // Try to merge on the left - if (previous && previous->m_data + previous->m_size == (u8*)target_entry) { - previous->m_size += sizeof(Node) + target_entry->m_size; - - // Additionally, try to merge on the right - if (previous->m_data + previous->m_size == (u8*)entry) { - previous->m_size += sizeof(Node) + entry->m_size; - previous->m_next = entry->m_next; - } - - return; - } - - // Try to merge on the right - if (target_entry->m_data + target_entry->m_size == (u8*)entry) { - target_entry->m_size += sizeof(Node) + entry->m_size; - target_entry->m_next = entry->m_next; - } else { - target_entry->m_next = entry; - } - - if (previous) - previous->m_next = target_entry; - else - m_freelist = target_entry; - - return; - } - - previous = entry; + // Additionally, try to merge on the right + if (previous->m_data + previous->m_size == (u8 *)entry) { + previous->m_size += sizeof(Node) + entry->m_size; + previous->m_next = entry->m_next; } + return; + } + + // Try to merge on the right + if (target_entry->m_data + target_entry->m_size == (u8 *)entry) { + target_entry->m_size += sizeof(Node) + entry->m_size; + target_entry->m_next = entry->m_next; + } else { + target_entry->m_next = entry; + } + + if (previous) + previous->m_next = target_entry; + else m_freelist = target_entry; + + return; } - u8* MemoryAllocator::reallocate(u8 *pointer, usize size, bool debug_override, void *address) - { - if (address == nullptr) - address = __builtin_return_address(0); + previous = entry; + } - if (m_debug && debug_override) - dbgln("\e[32mMTRACE: @ {} < {}\e[0m", address, pointer); + m_freelist = target_entry; +} - auto *entry = reinterpret_cast((u8*)pointer - sizeof(Node)); +u8 *MemoryAllocator::reallocate(u8 *pointer, usize size, bool debug_override, + void *address) { + if (address == nullptr) + address = __builtin_return_address(0); - if (size < entry->m_size) { - if (m_debug && debug_override) - dbgln("\e[32mMTRACE: @ {} > {} {}\e[0m", address, pointer, size); + if (m_debug && debug_override) + dbgln("\e[32mMTRACE: @ {} < {}\e[0m", address, pointer); - return pointer; - } + auto *entry = reinterpret_cast((u8 *)pointer - sizeof(Node)); - u8 *new_pointer = allocate(size, false); - memcpy(new_pointer, pointer, entry->m_size); - deallocate(pointer, false); + if (size < entry->m_size) { + if (m_debug && debug_override) + dbgln("\e[32mMTRACE: @ {} > {} {}\e[0m", address, pointer, size); - dbgln("\e[32mMTRACE: @ {} > {} {}\e[0m", address, new_pointer, size); + return pointer; + } - return new_pointer; - } + u8 *new_pointer = allocate(size, false); + memcpy(new_pointer, pointer, entry->m_size); + deallocate(pointer, false); + + dbgln("\e[32mMTRACE: @ {} > {} {}\e[0m", address, new_pointer, size); + + return new_pointer; } +} // namespace Std diff --git a/Std/MemoryAllocator.hpp b/Std/MemoryAllocator.hpp index 28596ea..99bacd5 100644 --- a/Std/MemoryAllocator.hpp +++ b/Std/MemoryAllocator.hpp @@ -2,66 +2,68 @@ #include -namespace Std -{ - class MemoryAllocator { - public: - explicit MemoryAllocator(Bytes heap); +namespace Std { +class MemoryAllocator { +public: + explicit MemoryAllocator(Bytes heap); - usize heap_size() const { return m_heap.size(); } + usize heap_size() const { return m_heap.size(); } - virtual u8* allocate(usize, bool debug_override = true, void *address = nullptr); - virtual void deallocate(u8*, bool debug_override = true, void *address = nullptr); - virtual u8* reallocate(u8*, usize, bool debug_override = true, void *address = nullptr); + virtual u8 *allocate(usize, bool debug_override = true, + void *address = nullptr); + virtual void deallocate(u8 *, bool debug_override = true, + void *address = nullptr); + virtual u8 *reallocate(u8 *, usize, bool debug_override = true, + void *address = nullptr); - void dump() - { - dbgln("m_freelist:"); - for (auto *entry = m_freelist; entry; entry = entry->m_next) - dbgln(" {} ({} bytes)", entry, entry->m_size); + void add_memory(Bytes bytes); - auto stats = statistics(); + void dump() { + dbgln("m_freelist:"); + for (auto *entry = m_freelist; entry; entry = entry->m_next) + dbgln(" {} ({} bytes)", entry, entry->m_size); - dbgln("statistics:"); - dbgln(" m_largest_continous_block {}", stats.m_largest_continous_block); - dbgln(" m_avaliable_memory {}", stats.m_avaliable_memory); - } + auto stats = statistics(); - struct Statistics { - usize m_largest_continous_block; - usize m_avaliable_memory; - }; + dbgln("statistics:"); + dbgln(" m_largest_continous_block {}", stats.m_largest_continous_block); + dbgln(" m_avaliable_memory {}", stats.m_avaliable_memory); + } - Statistics statistics() - { - Statistics stats; + struct Statistics { + usize m_largest_continous_block; + usize m_avaliable_memory; + }; - stats.m_largest_continous_block = 0; - stats.m_avaliable_memory = 0; + Statistics statistics() { + Statistics stats; - for (auto *entry = m_freelist; entry; entry = entry->m_next) { - stats.m_avaliable_memory += entry->m_size; + stats.m_largest_continous_block = 0; + stats.m_avaliable_memory = 0; - if (entry->m_size > stats.m_largest_continous_block) - stats.m_largest_continous_block = entry->m_size; - } + for (auto *entry = m_freelist; entry; entry = entry->m_next) { + stats.m_avaliable_memory += entry->m_size; - return stats; - } + if (entry->m_size > stats.m_largest_continous_block) + stats.m_largest_continous_block = entry->m_size; + } - bool m_debug = false; + return stats; + } - protected: - struct Node { - usize m_size; - Node *m_next; - u8 m_data[]; - }; - static_assert(sizeof(Node) % 4 == 0); + bool m_debug = false; - Bytes m_heap; +protected: + struct Node { + usize m_size; + Node *m_next; + u8 m_data[]; + }; + static_assert(sizeof(Node) % 4 == 0); - private: - Node *m_freelist; - }; -} + Bytes m_heap; + +private: + Node *m_freelist; +}; +} // namespace Std diff --git a/Std/Singleton.hpp b/Std/Singleton.hpp index 4878ba3..28e8856 100644 --- a/Std/Singleton.hpp +++ b/Std/Singleton.hpp @@ -2,51 +2,46 @@ #include -// FIXME: This is not safe for multiple cores or even multiple threads - -namespace Std -{ - template - struct SingletonContainer - { - static inline bool m_initialized = false; - - alignas(T) - static inline u8 m_instance[sizeof(T)]; - }; - - template - class Singleton { - public: - Singleton(const Singleton&) = delete; - Singleton(Singleton&&) = delete; - - Singleton() = default; - - template - static void initialize(Parameters&&... parameters) - { - VERIFY(!m_initialized); - - m_initialized = true; - new (m_instance) T { forward(parameters)... }; - } - - static bool is_initialized() - { - return m_initialized; - } - - static T& the() - { - VERIFY(m_initialized); - return *m_instance; - } - - private: - static inline bool& m_initialized = SingletonContainer::m_initialized; - - // FIXME: This is the incorrect type - static inline T (&m_instance)[] = reinterpret_cast(SingletonContainer::m_instance); - }; -} +namespace Std { +// Storage container to verify Type completeness on usage, not definition +template struct SingletonContainer { + static inline bool m_initialized = false; + alignas(T) static inline u8 m_instance_storage[sizeof(T)]; +}; + +// A singleton wrapper that manages the lifetime of a single instance. +// +// NOTE: This implementation is NOT thread-safe during initialization. +// It is intended to be initialized during the single-threaded boot phase +// of the kernel, before any secondary cores or interrupts are enabled. +// Access via `the()` is safe provided initialization is complete and +// the instance itself handles concurrency (or is immutable). +template class Singleton { +public: + Singleton(const Singleton &) = delete; + Singleton(Singleton &&) = delete; + Singleton &operator=(const Singleton &) = delete; + Singleton &operator=(Singleton &&) = delete; + + Singleton() = default; + + template + static void initialize(Parameters &&...parameters) { + VERIFY(!m_initialized); + // Construct in-place + new (SingletonContainer::m_instance_storage) + T{forward(parameters)...}; + m_initialized = true; + } + + static bool is_initialized() { return m_initialized; } + + static T &the() { + VERIFY(m_initialized); + return *reinterpret_cast(SingletonContainer::m_instance_storage); + } + +private: + static inline bool &m_initialized = SingletonContainer::m_initialized; +}; +} // namespace Std diff --git a/Std/SortedSet.hpp b/Std/SortedSet.hpp index c9f61ec..54aaf25 100644 --- a/Std/SortedSet.hpp +++ b/Std/SortedSet.hpp @@ -3,358 +3,422 @@ #include #include -namespace Std -{ - // FIXME: Upgrade to red/black trees - - template - class SortedSet { - public: - SortedSet() - { - m_root = nullptr; - m_size = 0; +namespace Std { +// A Red-Black Tree implementation of a sorted set. +// Invariants: +// 1. Every node is either red or black. +// 2. The root is always black. +// 3. Every leaf (NIL) is black. +// 4. If a node is red, then both its children are black. +// 5. For each node, all simple paths from the node to descendant leaves contain +// the same number of black nodes. +template class SortedSet { +public: + enum class Color { Red, Black }; + + struct Node { + T m_value; + Color m_color; + Node *m_left; + Node *m_right; + Node *m_parent; + + Node(const T &value) + : m_value(value), m_color(Color::Red), m_left(nullptr), + m_right(nullptr), m_parent(nullptr) {} + Node(T &&value) + : m_value(move(value)), m_color(Color::Red), m_left(nullptr), + m_right(nullptr), m_parent(nullptr) {} + + ~Node() { + delete m_left; + delete m_right; + } + + void dump(StringBuilder &builder) const { + if (!m_left && !m_right) { + builder.appendf("{}{}", m_value, m_color == Color::Red ? "(R)" : "(B)"); + return; + } + builder.append('('); + if (m_left) + m_left->dump(builder); + else + builder.append("nil"); + builder.appendf(" {}{} ", m_value, m_color == Color::Red ? "(R)" : "(B)"); + if (m_right) + m_right->dump(builder); + else + builder.append("nil"); + builder.append(')'); + } + }; + + SortedSet() : m_root(nullptr), m_size(0) {} + + ~SortedSet() { clear(); } + + SortedSet(const SortedSet &) = delete; + SortedSet &operator=(const SortedSet &) = delete; + + SortedSet(SortedSet &&other) : m_root(nullptr), m_size(0) { + *this = move(other); + } + + SortedSet &operator=(SortedSet &&other) { + clear(); + m_root = exchange(other.m_root, nullptr); + m_size = exchange(other.m_size, 0); + return *this; + } + + void clear() { + delete m_root; + m_root = nullptr; + m_size = 0; + } + + usize size() const { return m_size; } + + class InorderIterator { + public: + InorderIterator(SortedSet &set, Node *root) : m_set(set), m_current(root) {} + + InorderIterator begin() { return *this; } + InorderIterator end() { return {m_set, nullptr}; } + bool is_end() { return *this == end(); } + + const T &operator*() const { return m_current->m_value; } + T &operator*() { return m_current->m_value; } + + InorderIterator &operator++() { + if (!m_current) + return *this; + if (m_current->m_right) { + m_current = m_current->m_right; + while (m_current->m_left) + m_current = m_current->m_left; + return *this; + } + Node *p = m_current->m_parent; + while (p && m_current == p->m_right) { + m_current = p; + p = p->m_parent; + } + m_current = p; + return *this; + } + + InorderIterator operator++(int) { + InorderIterator copy = *this; + ++(*this); + return copy; + } + + bool operator==(const InorderIterator &other) const { + return m_current == other.m_current; + } + bool operator!=(const InorderIterator &other) const { + return !(*this == other); + } + + private: + SortedSet &m_set; + Node *m_current; + }; + + InorderIterator inorder() { + Node *min = m_root; + while (min && min->m_left) + min = min->m_left; + return InorderIterator{*this, min}; + } + + // Searches for a value in the set. + // Returns a pointer to the value if found, or nullptr otherwise. + // Complexity: O(log n) + T *search(const T &value) { + Node *current = m_root; + while (current) { + if (value < current->m_value) + current = current->m_left; + else if (current->m_value < value) + current = current->m_right; + else + return ¤t->m_value; + } + return nullptr; + } + + // Inserts a value into the set. + // If the value already exists, the existing node is effectively essentially + // "overwritten" or kept (set semantics typically imply unique keys, but this + // implementation updates the value if found in the loop before this helper). + // Wait, the current implementation *returns* existing if found? + // Ah, `insert_impl` replaces if found. + // Complexity: O(log n) + T &insert(const T &value) { return insert_impl(value); } + T &insert(T &&value) { return insert_impl(move(value)); } + + void remove(const T &value) { + Node *z = m_root; + while (z) { + // If value < z->m_value, go left. + // If z->m_value < value, go right. + // Else match. + if (value < z->m_value) + z = z->m_left; + else if (z->m_value < value) + z = z->m_right; + else + break; + } + if (!z) + return; + + Node *y = z; + Node *x; + Node *x_parent; + Color y_original_color = y->m_color; + + if (!z->m_left) { + x = z->m_right; + x_parent = z->m_parent; + transplant(z, z->m_right); + z->m_right = nullptr; + } else if (!z->m_right) { + x = z->m_left; + x_parent = z->m_parent; + transplant(z, z->m_left); + z->m_left = nullptr; + } else { + y = z->m_right; + while (y->m_left) + y = y->m_left; + y_original_color = y->m_color; + x = y->m_right; + + if (y->m_parent == z) { + x_parent = y; + } else { + x_parent = y->m_parent; + transplant(y, y->m_right); + y->m_right = z->m_right; + if (y->m_right) + y->m_right->m_parent = y; + } + transplant(z, y); + y->m_left = z->m_left; + y->m_left->m_parent = y; + y->m_color = z->m_color; + + z->m_left = nullptr; + z->m_right = nullptr; + } + + z->m_left = nullptr; + z->m_right = nullptr; + delete z; + m_size--; + + if (y_original_color == Color::Black) + delete_fixup(x, x_parent); + } + + void dump(StringBuilder &builder) const { + if (m_root) + m_root->dump(builder); + else + builder.append("nil"); + } + +private: + Node *m_root; + usize m_size; + + template T &insert_impl(T_ &&value) { + Node *z = new Node(forward(value)); + Node *y = nullptr; + Node *x = m_root; + + while (x) { + y = x; + if (z->m_value < x->m_value) + x = x->m_left; + else if (x->m_value < z->m_value) + x = x->m_right; + else { + x->m_value = forward(value); + delete z; + return x->m_value; + } + } + + z->m_parent = y; + if (!y) + m_root = z; + else if (z->m_value < y->m_value) + y->m_left = z; + else + y->m_right = z; + + m_size++; + insert_fixup(z); + return z->m_value; + } + + void left_rotate(Node *x) { + Node *y = x->m_right; + x->m_right = y->m_left; + if (y->m_left) + y->m_left->m_parent = x; + y->m_parent = x->m_parent; + if (!x->m_parent) + m_root = y; + else if (x == x->m_parent->m_left) + x->m_parent->m_left = y; + else + x->m_parent->m_right = y; + y->m_left = x; + x->m_parent = y; + } + + void right_rotate(Node *y) { + Node *x = y->m_left; + y->m_left = x->m_right; + if (x->m_right) + x->m_right->m_parent = y; + x->m_parent = y->m_parent; + if (!y->m_parent) + m_root = x; + else if (y == y->m_parent->m_right) + y->m_parent->m_right = x; + else + y->m_parent->m_left = x; + x->m_right = y; + y->m_parent = x; + } + + void insert_fixup(Node *z) { + while (z->m_parent && z->m_parent->m_color == Color::Red) { + if (z->m_parent == z->m_parent->m_parent->m_left) { + Node *y = z->m_parent->m_parent->m_right; + if (y && y->m_color == Color::Red) { + z->m_parent->m_color = Color::Black; + y->m_color = Color::Black; + z->m_parent->m_parent->m_color = Color::Red; + z = z->m_parent->m_parent; + } else { + if (z == z->m_parent->m_right) { + z = z->m_parent; + left_rotate(z); + } + z->m_parent->m_color = Color::Black; + z->m_parent->m_parent->m_color = Color::Red; + right_rotate(z->m_parent->m_parent); } - ~SortedSet() - { - clear(); + } else { + Node *y = z->m_parent->m_parent->m_left; + if (y && y->m_color == Color::Red) { + z->m_parent->m_color = Color::Black; + y->m_color = Color::Black; + z->m_parent->m_parent->m_color = Color::Red; + z = z->m_parent->m_parent; + } else { + if (z == z->m_parent->m_left) { + z = z->m_parent; + right_rotate(z); + } + z->m_parent->m_color = Color::Black; + z->m_parent->m_parent->m_color = Color::Red; + left_rotate(z->m_parent->m_parent); } - - // FIXME: Implement this - SortedSet(const SortedSet&) = delete; - - SortedSet(SortedSet&& other) - { - m_root = nullptr; - m_size = 0; - - *this = move(other); + } + } + m_root->m_color = Color::Black; + } + + void transplant(Node *u, Node *v) { + if (!u->m_parent) + m_root = v; + else if (u == u->m_parent->m_left) + u->m_parent->m_left = v; + else + u->m_parent->m_right = v; + if (v) + v->m_parent = u->m_parent; + } + + void delete_fixup(Node *x, Node *parent) { + while (x != m_root && (!x || x->m_color == Color::Black)) { + if (x == parent->m_left) { + Node *w = parent->m_right; + // w cannot be null if x is null (black height) + if (w->m_color == Color::Red) { + w->m_color = Color::Black; + parent->m_color = Color::Red; + left_rotate(parent); + w = parent->m_right; } - - struct Node { - Node(const T& value) - : m_value(value) - { - m_left = nullptr; - m_right = nullptr; - m_parent = nullptr; - } - Node(T&& value) - : m_value(move(value)) - { - m_left = nullptr; - m_right = nullptr; - m_parent = nullptr; - } - ~Node() - { - delete m_left; - delete m_right; - } - - void dump(StringBuilder& builder) const - { - if (m_left == nullptr && m_right == nullptr) { - builder.appendf("{}", m_value); - return; - } - - builder.append('('); - - if (m_left) - m_left->dump(builder); - else - builder.append("nil"); - - builder.appendf(" {} ", m_value); - - if (m_right) - m_right->dump(builder); - else - builder.append("nil"); - - builder.append(')'); - } - - void replace_child(Node *old, Node *new_) - { - if (m_left == old) { - if (m_left != nullptr) - m_left->m_parent = nullptr; - - m_left = new_; - if (m_left) - m_left->m_parent = this; - } else if (m_right == old) { - if (m_right != nullptr) - m_right->m_parent = nullptr; - - m_right = new_; - if (m_right) - m_right->m_parent = this; - } else { - ASSERT_NOT_REACHED(); - } - } - - T m_value; - Node *m_left; - Node *m_right; - Node *m_parent; - }; - - class InorderIterator { - public: - InorderIterator(SortedSet& set, Node *root) - : m_set(set) - , m_current(root) - { - } - - InorderIterator begin() { return *this; } - InorderIterator end() { return { m_set, nullptr }; } - - bool is_end() { return *this == end(); } - - const T& operator*() const { return m_current->m_value; } - T& operator*() { return m_current->m_value; } - - InorderIterator& operator++() - { - ASSERT(m_current); - - if (m_current->m_right != nullptr) { - Node *parent = nullptr; - m_current = m_set.min_impl(m_current->m_right, &parent); - - return *this; - } - - while (m_current->m_parent && m_current->m_parent->m_left != m_current) - m_current = m_current->m_parent; - - if (m_current->m_parent == nullptr) { - m_current = nullptr; - return *this; - } - - m_current = m_current->m_parent; - return *this; - } - InorderIterator operator++(int) - { - InorderIterator copy = *this; - operator++(); - return copy; - } - - bool operator==(InorderIterator& other) const - { - return m_current == other.m_current; - } - bool operator!=(InorderIterator& other) const - { - return !operator==(other); - } - - private: - SortedSet& m_set; - Node *m_current; - }; - - T* search(const T& value) - { - Node *parent = nullptr; - Node *node = search_impl(value, &parent, m_root); - - if (node != nullptr) - return &node->m_value; - else - return nullptr; - } - - void dump(StringBuilder& builder) const - { - if (m_root) - m_root->dump(builder); - else - builder.append("nil"); - } - - T& insert(const T& value) - { - return insert_impl(value); - } - T& insert(T&& value) - { - return insert_impl(move(value)); - } - - const T* min() const - { - return const_cast(this)->min(); + if ((!w->m_left || w->m_left->m_color == Color::Black) && + (!w->m_right || w->m_right->m_color == Color::Black)) { + w->m_color = Color::Red; + x = parent; + parent = x->m_parent; + } else { + if (!w->m_right || w->m_right->m_color == Color::Black) { + if (w->m_left) + w->m_left->m_color = Color::Black; + w->m_color = Color::Red; + right_rotate(w); + w = parent->m_right; + } + w->m_color = parent->m_color; + parent->m_color = Color::Black; + if (w->m_right) + w->m_right->m_color = Color::Black; + left_rotate(parent); + x = m_root; + // parent doesn't matter, loop terminates } - T* min() - { - Node *parent = nullptr; - Node *node = min_impl(m_root, &parent); - - if (node) - return &node->m_value; - else - return nullptr; - } - - void remove(const T& value) - { - Node *parent = nullptr; - Node *node = search_impl(value, &parent, m_root); - - if (node == nullptr) - return; - - if (node->m_left == nullptr || node->m_right == nullptr) - return remove_impl(node, parent); - - Node *min_in_right_parent = node; - Node *min_in_right = min_impl(node->m_right, &min_in_right_parent); - ASSERT(min_in_right); - - node->m_value = move(min_in_right->m_value); - - remove_impl(min_in_right, min_in_right_parent); - } - - InorderIterator inorder() - { - Node *parent = nullptr; - Node *min = min_impl(m_root, &parent); - - return InorderIterator { *this, min }; + } else { + Node *w = parent->m_left; + if (w->m_color == Color::Red) { + w->m_color = Color::Black; + parent->m_color = Color::Red; + right_rotate(parent); + w = parent->m_left; } - - usize size() const { return m_size; } - - void clear() - { - delete m_root; - - m_root = nullptr; - m_size = 0; - } - - SortedSet& operator=(SortedSet&& other) - { - clear(); - - m_root = exchange(other.m_root, nullptr); - m_size = exchange(other.m_size, 0); - - return *this; - } - - private: - template - T& insert_impl(T_&& value) - { - Node *parent = nullptr; - Node *node = search_impl(value, &parent, m_root); - - if (node) { - node->m_value = forward(value); - - return node->m_value; - } else if (parent == nullptr) { - node = m_root = new Node { forward(value) }; - ++m_size; - - return node->m_value; - } else if (value < parent->m_value) { - ASSERT(parent->m_left == nullptr); - node = parent->m_left = new Node { forward(value) }; - node->m_parent = parent; - ++m_size; - - return node->m_value; - } else { - ASSERT(value > parent->m_value); - - ASSERT(parent->m_right == nullptr); - node = parent->m_right = new Node { forward(value) }; - node->m_parent = parent; - ++m_size; - - return node->m_value; - } - } - - Node* search_impl(const T& value, Node **parent, Node *subtree) - { - if (subtree == nullptr) - return nullptr; - - if (value < subtree->m_value) { - *parent = subtree; - return search_impl(value, parent, subtree->m_left); - } else if (value > subtree->m_value) { - *parent = subtree; - return search_impl(value, parent, subtree->m_right); - } else { - return subtree; - } - } - - Node* min_impl(Node* subtree, Node **parent) - { - if (subtree == nullptr) - return nullptr; - - if (subtree->m_left == nullptr) - return subtree; - - *parent = subtree; - return min_impl(subtree->m_left, parent); - } - - void remove_impl(Node *node, Node *parent) - { - ASSERT(node->m_left == nullptr || node->m_right == nullptr); - - if (node->m_left == nullptr && node->m_right == nullptr) { - if (parent) - parent->replace_child(node, nullptr); - else - m_root = nullptr; - } else if (node->m_left == nullptr) { - if (parent) - parent->replace_child(node, node->m_right); - else - m_root = node->m_right; - - node->m_right = nullptr; - } else { - if (parent) - parent->replace_child(node, node->m_left); - else - m_root = node->m_left; - - node->m_left = nullptr; - } - - node->m_parent = nullptr; - delete node; - --m_size; - } - - Node *m_root; - usize m_size; - }; - - template - struct Formatter> { - static void format(StringBuilder& builder, const SortedSet& value) - { - return value.dump(builder); + if ((!w->m_right || w->m_right->m_color == Color::Black) && + (!w->m_left || w->m_left->m_color == Color::Black)) { + w->m_color = Color::Red; + x = parent; + parent = x->m_parent; + } else { + if (!w->m_left || w->m_left->m_color == Color::Black) { + if (w->m_right) + w->m_right->m_color = Color::Black; + w->m_color = Color::Red; + left_rotate(w); + w = parent->m_left; + } + w->m_color = parent->m_color; + parent->m_color = Color::Black; + if (w->m_left) + w->m_left->m_color = Color::Black; + right_rotate(parent); + x = m_root; } - }; -} + } + } + if (x) + x->m_color = Color::Black; + } +}; + +template struct Formatter> { + static void format(StringBuilder &builder, const SortedSet &value) { + value.dump(builder); + } +}; +} // namespace Std diff --git a/Std/Vector.hpp b/Std/Vector.hpp index 8d0c7b0..38912af 100644 --- a/Std/Vector.hpp +++ b/Std/Vector.hpp @@ -1,168 +1,178 @@ #pragma once +#include #include #include -#include -namespace Std -{ - template - class Vector { - public: - Vector() - { - m_use_inline_data = true; - m_size = 0; - m_capacity = InlineSize; - m_data = nullptr; - } - ~Vector() - { - clear(); - - if (m_data != nullptr) { - operator delete[](m_data); - m_data = nullptr; - } - } - Vector(const Vector& other) - : Vector() - { - *this = other; - } - Vector(Vector&& other) - : Vector() - { - *this = move(other); - } - - T& append(const T& value) - { - ensure_capacity(m_size + 1); - - T *pointer = new (data() + m_size) T { value }; - ++m_size; - - return *pointer; - } - T& append(T&& value) - { - ensure_capacity(m_size + 1); - - T *pointer = new (data() + m_size) T { move(value) }; - ++m_size; - - return *pointer; - } - - void extend(Span values) - { - ensure_capacity(m_size + values.size()); - - for (auto& value : values.iter()) - append(value); - } - - void ensure_capacity(usize new_capacity) - { - if (m_capacity >= new_capacity) - return; - - new_capacity = round_to_power_of_two(new_capacity); - - T *new_data = reinterpret_cast(new u8[sizeof(T) * new_capacity]); - ASSERT(new_data != nullptr); - - ASSERT(new_capacity > m_size); - - if constexpr (Concepts::Primitive) { - memcpy(new_data, data(), m_size * sizeof(T)); - } else { - // Otherwise, we will get misaligned read/write problems here - ASSERT(sizeof(T) % 4 == 0); - - for (usize i = 0; i < m_size; ++ i) { - new (new_data + i) T { move(data()[i]) }; - data()[i].~T(); - } - } - - operator delete[](m_data); - - m_data = new_data; - m_capacity = new_capacity; - m_use_inline_data = false; - } - - const T* data() const - { - if (m_use_inline_data) - return reinterpret_cast(m_inline_data); - return m_data; - } - T* data() - { - if (m_use_inline_data) - return reinterpret_cast(m_inline_data); - return m_data; - } - - usize size() const { return m_size; } - usize capacity() const { return m_capacity; } - - Span span() { return { data(), size() }; } - Span span() const { return { data(), size() }; } - - SpanIterator iter() { return span().iter(); } - SpanIterator iter() const { return span().iter(); } - - const T& operator[](usize index) const { return data()[index]; } - T& operator[](usize index) { return data()[index]; } - - void clear() - { - for (usize index = 0; index < m_size; ++index) - data()[index].~T(); - - m_size = 0; - } - - Vector& operator=(const Vector& other) - { - clear(); - extend(other.span()); - return *this; - } - Vector& operator=(Vector&& other) - { - clear(); - - if (other.m_use_inline_data) { - extend(other.span()); - other.clear(); - } else { - operator delete[](m_data); - - m_use_inline_data = false; - m_capacity = other.m_capacity; - m_size = other.m_size; - m_data = other.m_data; - - other.m_use_inline_data = true; - other.m_capacity = InlineSize; - other.m_size = 0; - other.m_data = nullptr; - } - - return *this; - } - - private: - bool m_use_inline_data; - usize m_size; - usize m_capacity; - - u8 m_inline_data[sizeof(T) * InlineSize]; - T *m_data; - }; -} +namespace Std { +template class Vector { +public: + // -- Constructors & Destructor -- + + constexpr Vector() + : m_size(0), m_capacity(InlineSize), m_use_inline_data(true), + m_data(nullptr) {} + + ~Vector() { + clear(); + if (!m_use_inline_data && m_data) { + operator delete[](m_data); + } + } + + Vector(const Vector &other) : Vector() { *this = other; } + + Vector(Vector &&other) noexcept : Vector() { *this = move(other); } + + // -- Assignments -- + + Vector &operator=(const Vector &other) { + if (this == &other) + return *this; + clear(); + ensure_capacity(other.size()); + for (usize i = 0; i < other.size(); ++i) { + new (data() + i) T(other[i]); + } + m_size = other.size(); + return *this; + } + + Vector &operator=(Vector &&other) noexcept { + if (this == &other) + return *this; + clear(); + + if (other.m_use_inline_data) { + // If other is using inline data, we must copy it because we can't "steal" + // the buffer embedded in the other object. + ensure_capacity(other.size()); + for (usize i = 0; i < other.size(); ++i) { + new (data() + i) T(move(other[i])); + } + m_size = other.size(); + } else { + // Steal the heap pointer + if (!m_use_inline_data) + operator delete[](m_data); + + m_data = other.m_data; + m_size = other.m_size; + m_capacity = other.m_capacity; + m_use_inline_data = false; + + // Reset other + other.m_data = nullptr; + other.m_size = 0; + other.m_capacity = InlineSize; + other.m_use_inline_data = true; + } + other.clear(); + return *this; + } + + // -- Element Access -- + + T &append(const T &value) { + ensure_capacity(m_size + 1); + new (data() + m_size) T(value); + return data()[m_size++]; + } + + T &append(T &&value) { + ensure_capacity(m_size + 1); + new (data() + m_size) T(move(value)); + return data()[m_size++]; + } + + void extend(Span values) { + ensure_capacity(m_size + values.size()); + for (auto &value : values.iter()) { + append(value); + } + } + + [[nodiscard]] const T &operator[](usize index) const { + ASSERT(index < m_size); + return data()[index]; + } + + [[nodiscard]] T &operator[](usize index) { + ASSERT(index < m_size); + return data()[index]; + } + + [[nodiscard]] T &at(usize index) { + // TODO: Throw exception or panic if out of bounds (ASSERT for now) + ASSERT(index < m_size); + return data()[index]; + } + + // -- Capacity & Size -- + + [[nodiscard]] usize size() const { return m_size; } + [[nodiscard]] usize capacity() const { return m_capacity; } + [[nodiscard]] bool is_empty() const { return m_size == 0; } + + void ensure_capacity(usize needed_capacity) { + if (m_capacity >= needed_capacity) + return; + + usize new_capacity = m_capacity == 0 ? 4 : m_capacity; + while (new_capacity < needed_capacity) { + new_capacity *= 2; + } + + T *new_buffer = static_cast(operator new[](new_capacity * sizeof(T))); + + // Move existing elements + for (usize i = 0; i < m_size; ++i) { + new (new_buffer + i) T(move(data()[i])); + data()[i].~T(); + } + + if (!m_use_inline_data && m_data) { + operator delete[](m_data); + } + + m_data = new_buffer; + m_capacity = new_capacity; + m_use_inline_data = false; + } + + void clear() { + for (usize i = 0; i < m_size; ++i) { + data()[i].~T(); + } + m_size = 0; + } + + // -- Iterators -- + + T *begin() { return data(); } + T *end() { return data() + m_size; } + const T *begin() const { return data(); } + const T *end() const { return data() + m_size; } + + Span span() { return {data(), size()}; } + Span span() const { return {data(), size()}; } + SpanIterator iter() { return span().iter(); } + SpanIterator iter() const { return span().iter(); } + + T *data() { + return m_use_inline_data ? reinterpret_cast(m_inline_data) : m_data; + } + const T *data() const { + return m_use_inline_data ? reinterpret_cast(m_inline_data) + : m_data; + } + +private: + usize m_size; + usize m_capacity; + bool m_use_inline_data; + + T *m_data; + alignas(T) u8 m_inline_data[sizeof(T) * (InlineSize > 0 ? InlineSize : 1)]; +}; +} // namespace Std diff --git a/TODO.md b/TODO.md index 881b745..c843a1a 100644 --- a/TODO.md +++ b/TODO.md @@ -1,101 +1,92 @@ -### TODO - -#### Next Version - -- Add `SoftwareSpinLock` that uses a hardware spin lock but there can be more than actual hardware spin locks. - This is an active locking primitive that must only be used with interrupts disabled. - It should be simple to add deadlock detection here. - -- Add `SoftwareMutex` that uses a `HardwareSpinLock` internally. - This is a passive locking primitive that must only be used with interrupts enabled. - -- Add `WaitingThreadQueue` that keeps a list of threads that are waiting for some resource. - This must only be used with interrupts disabled and uses a `HardwareSpinLock` internally. - -- Verify that all of these locking primitives are functional. - -- Protect all the resources with these locking primitives. - -- Schedule on both cores. - -### Old - -#### Next Version - -- Group `PageRange`s together in `PageAllocator::deallocate`. - -- Add passive locking primitives - -- Add active locking primitives - -#### Bugs - -- We have a ton of memory leaks in the filesystem, e.g. `VirtualFile::create_handle_impl`. - - - If we do `stat /dev/tty` we get invalid information, because `ConsoleFileHandle` always - returns `ConsoleFile` instead of the actual file. - - - Sometimes we seem to mess something up, this is visible when `ConsoleFileHandle` has an invalid - `this` pointer. I reproduced this by running: - - ~~~none - Example.elf - Example.elf - Example.elf - Example.elf - Example.elf - Example.elf - Example.elf - ~~~ - -#### Future features - -- Keep track of 'used' page ranges. There is an excelent algorithm that can be used to store these bits - in a very compact tree structure. - -- Maybe I could port the Minix filesystem when I add an IDE driver? - -- Keep documentation about interrupt safe functions and which functions can be called in which boot stage - -- Add `MemoryAllocator::allocate_eternal` which doesn't create MTRACE logs - - - Run inside QEMU - - - Write userland applications in Zig - -#### Future tweaks (Userland) - - - Implement a proper malloc - -#### Future tweaks (Kernel) - - - Setup MPU for supervisor mode - - - HardFault in usermode crashes kernel - - - Stack smash protection with MPU - - - Build with `-fstack-protector`? - -#### Future tweaks (Build) - - - Alignment of `.stack`, `.heap` sections is lost in `readelf` - - - C++20 modules - - - Drop SDK entirely - - - Link `libsup++` or add a custom downcast? - - - Meson build - - - Try using LLDB instead of GDB - - - Don't leak includes from newlib libc - - - Use LLVM/LLD for `FileEmbed`; Not sure what I meant with this, but LLVM - surely has all the tools buildin that I need - - - GDB apparently has a secret 'proc' command that makes it possible to debug - multiple processes. This was mentioned in the DragonFlyBSD documentation, - keyword: "inferiour" +# TODO + +## ✅ Completed (2025-12-14) + +- [x] Refactor `Tools/` directory (ElfEmbed, FileSystem) to use modern C++20 and `std::filesystem`. +- [x] Verify and fix build process on Ubuntu/Debian. +- [x] Implement dynamic kernel heap growth. +- [x] Streamline `README.md` installation instructions. +- [x] Update `CHANGELOG.md` with changes since last release. + +## ✅ Completed (2025-12-12) + +- [x] macOS build compatibility (ELF definitions, portable file I/O) +- [x] Fix missing `MaskedInterruptGuard` includes across kernel +- [x] Fix stack alignment issues causing potential hardfaults +- [x] Implement `init_array`/`fini_array` calling in crt0 (C++ static constructor support) +- [x] Fix linker warnings (executable stack, missing symbols) +- [x] All unit tests passing (18/18) +- [x] Added `AbstractLock` and `LockGuard` +- [x] Added `HardwareSpinLock` using memory-mapped spin locks + +### Synchronization Primitives + +- [x] Add `SoftwareSpinLock` that uses a hardware spin lock but allows more than actual hardware spin locks. + - Active locking primitive, must only be used with interrupts disabled. + - [x] Should include deadlock detection. + - [x] Includes `wfe`/`sev` for power efficiency. + +- [x] Add `SoftwareMutex` that uses a `HardwareSpinLock` internally. + - Passive locking primitive, must only be used with interrupts enabled. + +- [x] Add `WaitingThreadQueue` that keeps a list of threads waiting for a resource. + - Must only be used with interrupts disabled, uses `HardwareSpinLock` internally. + +- [x] Verify all locking primitives are functional. + +- [x] Protect all shared resources with these locking primitives. + +### Multi-Core Support + +- [x] Schedule on both cores. +- [x] Fix `stat /dev/tty`. +- [x] Implement proper malloc (free-list). +- [x] Upgrade sorted set to RB Tree. +- [x] Track allocated pages in `PageAllocator`. + +--- + +## 🐛 Known Bugs + +- [x] Memory leaks in filesystem (e.g., `VirtualFile::create_handle_impl`). + +- [x] `stat /dev/tty` returns invalid information because `ConsoleFileHandle` always returns `ConsoleFile` instead of actual file. + +- [x] Intermittent `ConsoleFileHandle` invalid `this` pointer when running: + ``` + Example.elf + Example.elf + Example.elf + ... (repeated) + ``` + +--- + +## 📋 Future Features + +### Kernel: +- [x] Keep track of 'used' page ranges with compact tree structure +- [ ] Port Minix filesystem when IDE driver is added +- [x] Document interrupt-safe functions and boot stage compatibility +- [x] Add `MemoryAllocator::allocate_eternal` without MTRACE logs +- [ ] Run inside Emulation (Blocked: Mainline QEMU/Renode issues, see [Docs/Emulation_Status.md](Docs/Emulation_Status.md)) +- [x] Setup MPU for supervisor mode +- [x] HardFault in usermode should not crash kernel +- [x] Stack smash protection with MPU (`-fstack-protector`) and ROSC randomization +- [x] Group `PageRange`s together in using Buddy coalescing +- [x] Fix alignment of `.stack`, `.heap` sections in `linker.ld` + +### Userland: +- [x] Implement a proper malloc (current is bump allocator with no free) +- [ ] Write userland applications in Zig + +### Build System +- [x] Group `PageRange`s together in `PageAllocator::deallocate` (Buddy optimization implemented) +- [x] Fix alignment of `.stack`, `.heap` sections in `readelf` (Verified 8-byte alignment) +- [ ] C++20 modules support +- [ ] Drop SDK entirely (link `libsup++` or add custom downcast) +- [ ] Meson build support +- [ ] Try LLDB instead of GDB +- [ ] Don't leak includes from newlib libc +- [x] Use LLVM/LLD for `FileEmbed` (Evaluated: Custom Tool `Tools/ElfEmbed.cpp` needed for FileSystem structure) +- [ ] Explore GDB 'proc' command for multi-process debugging ("inferior") FileSystem structure diff --git a/Tests/Std/TestMemoryAllocator.cpp b/Tests/Std/TestMemoryAllocator.cpp index 362244c..5e68064 100644 --- a/Tests/Std/TestMemoryAllocator.cpp +++ b/Tests/Std/TestMemoryAllocator.cpp @@ -53,7 +53,7 @@ TEST_CASE(memoryallocator_death_by_a_thousand_cuts) ASSERT(stats_middle.m_largest_continous_block < heap.size() / 4); // We want to deallocate in a random, but reproducible order - std::mt19937 prng { 3040088493306752707 }; + std::mt19937 prng { static_cast(3040088493306752707) }; std::shuffle(allocations.begin(), allocations.end(), prng); for (u8 *allocation : allocations) diff --git a/Tests/Std/TestStringBuilder.cpp b/Tests/Std/TestStringBuilder.cpp index bab6480..7a2fc87 100644 --- a/Tests/Std/TestStringBuilder.cpp +++ b/Tests/Std/TestStringBuilder.cpp @@ -29,7 +29,7 @@ TEST_CASE(stringbuilder) builder.append(' '); builder.appendf("b{}z", "a"); - ASSERT(string_1.size() == string_1.view().size()); + ASSERT(builder.view() == "foo bar baz"); ASSERT(builder.string().view() == "foo bar baz"); diff --git a/Tests/Std/TestStringView.cpp b/Tests/Std/TestStringView.cpp index 6659a0e..b17f7fc 100644 --- a/Tests/Std/TestStringView.cpp +++ b/Tests/Std/TestStringView.cpp @@ -36,7 +36,7 @@ TEST_CASE(stringview_equal) Std::StringView sv1 = "foo"; Std::StringView sv2 = "bar"; Std::StringView sv3 = sv2; - Std::StringView sv4 = "foobar" + 3; + Std::StringView sv4 = &"foobar"[3]; ASSERT(sv1 != sv2); ASSERT(sv2 == sv3); diff --git a/Tools/CMakeLists.txt b/Tools/CMakeLists.txt index d976586..68a5d81 100644 --- a/Tools/CMakeLists.txt +++ b/Tools/CMakeLists.txt @@ -1,12 +1,18 @@ cmake_minimum_required(VERSION 3.19.5) project(Tools CXX) -find_package(fmt) +include(FetchContent) +FetchContent_Declare( + fmt + GIT_REPOSITORY https://github.com/fmtlib/fmt.git + GIT_TAG 11.0.2 +) +FetchContent_MakeAvailable(fmt) add_library(project_options INTERFACE) target_compile_features(project_options INTERFACE cxx_std_20) target_compile_options(project_options INTERFACE -fdiagnostics-color=always -g) -target_include_directories(project_options INTERFACE ${CMAKE_SOURCE_DIR} ${CMAKE_SOURCE_DIR}/..) +target_include_directories(project_options INTERFACE ${CMAKE_SOURCE_DIR} ${CMAKE_SOURCE_DIR}/.. ${CMAKE_SOURCE_DIR}/compat) target_compile_definitions(project_options INTERFACE HOST) file(GLOB LibElf_SOURCES LibElf/*.cpp) @@ -15,4 +21,4 @@ target_link_libraries(LibElf project_options fmt::fmt) file(GLOB ElfEmbed_SOURCES *.cpp) add_executable(ElfEmbed ${ElfEmbed_SOURCES}) -target_link_libraries(ElfEmbed project_options LibElf bsd) +target_link_libraries(ElfEmbed project_options LibElf) diff --git a/Tools/ElfEmbed.cpp b/Tools/ElfEmbed.cpp index ca9b550..6310601 100644 --- a/Tools/ElfEmbed.cpp +++ b/Tools/ElfEmbed.cpp @@ -1,8 +1,11 @@ +#include +#include +#include #include #include -#include #include +#include #include #include @@ -13,36 +16,66 @@ #include "FileSystem.hpp" -static void write_output_file(std::filesystem::path path, Elf::MemoryStream& stream) -{ - fmt::print("Writing output file {}\n", path.string()); +// Writes the generated ELF stream to the specified output path +static void write_output_file(const std::filesystem::path &path, + Elf::MemoryStream &stream) { + fmt::print("Writing output file {}\n", path.string()); - int fd = creat(path.c_str(), S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH); - assert(fd >= 0); + std::ofstream outfile(path, + std::ios::binary | std::ios::out | std::ios::trunc); + if (!outfile.is_open()) { + fmt::print(stderr, "Error: Could not open {} for writing.\n", + path.string()); + exit(1); + } - stream.copy_to_raw_fd(fd); - - int retval = close(fd); - assert(retval == 0); + // Create a buffer to hold the data + stream.copy_to_stream(outfile); + outfile.close(); } -int main(int argc, char **argv) -{ - // FIXME: Parse command line arguments +int main(int argc, char **argv) { + if (argc < 3) { + fmt::print(stderr, + "Usage: {} [input_file2 ...]\n", + argv[0]); + return 1; + } + + std::filesystem::path output_path = argv[1]; + std::vector input_files; + + for (int i = 2; i < argc; ++i) { + input_files.emplace_back(argv[i]); + } + + Elf::Generator generator; + FileSystem fs{generator}; + + std::map bin_files; - Elf::Generator generator; + for (const auto &file_path : input_files) { + if (!std::filesystem::exists(file_path)) { + fmt::print(stderr, "Error: Input file {} does not exist.\n", + file_path.string()); + return 1; + } - FileSystem fs { generator }; + std::string filename = file_path.filename().string(); + fmt::print("Embedding {}\n", filename); - std::map bin_files; - bin_files["Shell.elf"] = fs.add_host_file("Shell.elf", Kernel::ModeFlags::Regular | Kernel::ModeFlags::DefaultExecutablePermissions); - bin_files["Example.elf"] = fs.add_host_file("Example.elf", Kernel::ModeFlags::Regular | Kernel::ModeFlags::DefaultExecutablePermissions); - bin_files["Editor.elf"] = fs.add_host_file("Editor.elf", Kernel::ModeFlags::Regular | Kernel::ModeFlags::DefaultExecutablePermissions); + // Add file to filesystem (executable permissions by default for these + // tools) + bin_files[filename] = fs.add_host_file( + file_path, Kernel::ModeFlags::Regular | + Kernel::ModeFlags::DefaultExecutablePermissions); + } - fs.add_root_directory(bin_files); + fs.add_root_directory(bin_files); + fs.finalize(); - fs.finalize(); + auto stream = generator.finalize(); + write_output_file(output_path, stream); - auto stream = generator.finalize(); - write_output_file("FileSystem.elf", stream); + return 0; } diff --git a/Tools/FileSystem.cpp b/Tools/FileSystem.cpp index 59a24c0..81049c2 100644 --- a/Tools/FileSystem.cpp +++ b/Tools/FileSystem.cpp @@ -1,150 +1,171 @@ +#include #include +#include -#include -#include #include +#include +#include -#include +#include // fmt::format #include #include "FileSystem.hpp" -// FIXME: This is a huge mess, I need to completely rewrite this - -FileSystem::FileSystem(Elf::Generator& generator) - : m_generator(generator) -{ - m_data_index = m_generator.create_section(".embed", SHT_PROGBITS, SHF_ALLOC); - m_data_relocs.emplace(m_generator, ".embed", generator.symtab().symtab_index(), *m_data_index); - - m_base_symbol = m_generator.symtab().add_symbol("__flash_base", Elf32_Sym { - .st_value = 0, - .st_size = 0, - .st_info = ELF32_ST_INFO(STB_GLOBAL, STT_OBJECT), - .st_other = STV_DEFAULT, - .st_shndx = static_cast(*m_data_index), - }); +// FileSystem generator for embedding files into the kernel ELF + +FileSystem::FileSystem(Elf::Generator &generator) : m_generator(generator) { + m_data_index = m_generator.create_section(".embed", SHT_PROGBITS, SHF_ALLOC); + m_data_relocs.emplace(m_generator, ".embed", + generator.symtab().symtab_index(), *m_data_index); + + m_base_symbol = m_generator.symtab().add_symbol( + "__flash_base", Elf32_Sym{ + .st_value = 0, + .st_size = 0, + .st_info = ELF32_ST_INFO(STB_GLOBAL, STT_OBJECT), + .st_other = STV_DEFAULT, + .st_shndx = static_cast(*m_data_index), + }); } -FileSystem::~FileSystem() -{ - assert(m_finalized); -} -uint32_t FileSystem::add_host_file(std::string_view path, Kernel::ModeFlags mode) -{ - // FIXME: We don't have to map the file into memory - Elf::MemoryStream stream; - stream.write_bytes(Elf::mmap_file(path)); - return add_file(stream, mode); +FileSystem::~FileSystem() { + assert(m_finalized && "FileSystem was not finalized before destruction!"); } -uint32_t FileSystem::add_directory(std::map& files, uint32_t inode_number) -{ - Elf::MemoryStream stream; - std::vector directory_relocations; +uint32_t FileSystem::add_host_file(const std::filesystem::path &path, + Kernel::ModeFlags mode) { + if (!std::filesystem::exists(path)) { + fmt::print(stderr, "Error: File {} does not exist.\n", path.string()); + exit(1); + } - for (auto& [name, inode] : files) { - Kernel::FlashDirectoryInfo info; + // Memory-map file for efficient reading + Elf::MemoryStream stream; + // Note: ensure mmap_file handles std::filesystem::path or string properly + // Assuming mmap_file takes a string path. + stream.write_bytes(Elf::mmap_file(path.string())); - strlcpy(info.m_name, name.c_str(), sizeof(info.m_name)); - info.m_info_raw = m_inode_to_offset[inode]; + return add_file(stream, mode); +} - size_t info_offset = stream.write_object(info); +uint32_t FileSystem::add_directory(std::map &files, + uint32_t inode_number) { + Elf::MemoryStream stream; + std::vector directory_relocations; - directory_relocations.push_back(Elf32_Rel { - .r_offset = (uint32_t) (info_offset + offsetof(Kernel::FlashDirectoryInfo, m_info_raw)), - .r_info = ELF32_R_INFO(*m_base_symbol, R_ARM_ABS32), - }); - } + for (const auto &[name, inode] : files) { + Kernel::FlashDirectoryInfo info; - size_t load_offset; - inode_number = add_file(stream, Kernel::ModeFlags::Directory, inode_number, &load_offset); + // Securely copy filename, ensuring null termination + strncpy(info.m_name, name.c_str(), sizeof(info.m_name) - 1); + info.m_name[sizeof(info.m_name) - 1] = '\0'; - for (auto& relocation : directory_relocations) { - relocation.r_offset += load_offset; - m_data_relocs->add_entry(relocation); - } + info.m_info_raw = m_inode_to_offset[inode]; - return inode_number; -} -uint32_t FileSystem::add_root_directory(std::map& files) -{ - return add_directory(files, 2); -} -uint32_t FileSystem::add_file(Elf::MemoryStream& stream, Kernel::ModeFlags mode, uint32_t inode_number, size_t *load_offset) -{ - if (inode_number == 0) - inode_number = m_next_inode++; - - size_t data_offset = m_data_stream.write_bytes(stream); - - if (load_offset) - *load_offset = data_offset; - - size_t data_symbol = m_generator.symtab().add_symbol(fmt::format("__flash_data_{}", inode_number), Elf32_Sym { - .st_value = (uint32_t)data_offset, - .st_size = (uint32_t)stream.size(), - .st_info = ELF32_ST_INFO(STB_GLOBAL, STT_OBJECT), - .st_other = STV_DEFAULT, - .st_shndx = static_cast(*m_data_index), - }); + size_t info_offset = stream.write_object(info); - Kernel::FileInfo inode; - inode.st_dev = Kernel::FileSystemId::Flash; - inode.st_ino = inode_number; - inode.st_mode = mode; - inode.st_rdev = 0; - inode.st_size = stream.size(); - inode.st_uid = 0; - inode.st_gid = 0; - - // FIXME - inode.st_blksize = 0; - inode.st_blocks = 0; - - std::vector inode_relocations; - - inode.m_data = 0; - inode_relocations.push_back(Elf32_Rel { - .r_offset = offsetof(Kernel::FileInfo, m_data), - .r_info = ELF32_R_INFO(data_symbol, R_ARM_ABS32), + directory_relocations.push_back(Elf32_Rel{ + .r_offset = + (uint32_t)(info_offset + + offsetof(Kernel::FlashDirectoryInfo, m_info_raw)), + .r_info = ELF32_R_INFO(*m_base_symbol, R_ARM_ABS32), }); + } - size_t inode_offset = m_data_stream.write_object(inode); - size_t inode_symbol = m_generator.symtab().add_symbol(fmt::format("__flash_info_{}", inode_number), Elf32_Sym { - .st_value = static_cast(inode_offset), - .st_size = sizeof(Kernel::FileInfo), - .st_info = ELF32_ST_INFO(STB_GLOBAL, STT_OBJECT), - .st_other = STV_DEFAULT, - .st_shndx = static_cast(*m_data_index), - }); + size_t load_offset; + inode_number = add_file(stream, Kernel::ModeFlags::Directory, inode_number, + &load_offset); + + for (auto &relocation : directory_relocations) { + relocation.r_offset += load_offset; + m_data_relocs->add_entry(relocation); + } + + return inode_number; +} + +uint32_t +FileSystem::add_root_directory(std::map &files) { + return add_directory(files, 2); +} - if (inode_number == 2) { - m_generator.symtab().add_symbol("__flash_root", Elf32_Sym { - .st_value = static_cast(inode_offset), - .st_size = sizeof(Kernel::FileInfo), - .st_info = ELF32_ST_INFO(STB_GLOBAL, STT_OBJECT), - .st_other = STV_DEFAULT, - .st_shndx = static_cast(*m_data_index), - }); - } - - for (Elf32_Rel& relocation : inode_relocations) - { - relocation.r_offset += inode_offset; - m_data_relocs->add_entry(relocation); - } - - m_inode_to_offset[inode_number] = inode_offset; - - return inode.st_ino; +uint32_t FileSystem::add_file(Elf::MemoryStream &stream, Kernel::ModeFlags mode, + uint32_t inode_number, size_t *load_offset) { + if (inode_number == 0) + inode_number = m_next_inode++; + + size_t data_offset = m_data_stream.write_bytes(stream); + + if (load_offset) + *load_offset = data_offset; + + size_t data_symbol = m_generator.symtab().add_symbol( + fmt::format("__flash_data_{}", inode_number), + Elf32_Sym{ + .st_value = (uint32_t)data_offset, + .st_size = (uint32_t)stream.size(), + .st_info = ELF32_ST_INFO(STB_GLOBAL, STT_OBJECT), + .st_other = STV_DEFAULT, + .st_shndx = static_cast(*m_data_index), + }); + + Kernel::FileInfo inode{.st_dev = Kernel::FileSystemId::Flash, + .st_ino = inode_number, + .st_mode = mode, + .st_rdev = 0, + .st_size = static_cast(stream.size()), + .st_blksize = 0, + .st_blocks = 0, + .st_uid = 0, + .st_gid = 0, + .m_data = 0}; + + std::vector inode_relocations; + + inode_relocations.push_back(Elf32_Rel{ + .r_offset = offsetof(Kernel::FileInfo, m_data), + .r_info = ELF32_R_INFO(data_symbol, R_ARM_ABS32), + }); + + size_t inode_offset = m_data_stream.write_object(inode); + + // Unused symbol, kept for debugging mapping + m_generator.symtab().add_symbol( + fmt::format("__flash_info_{}", inode_number), + Elf32_Sym{ + .st_value = static_cast(inode_offset), + .st_size = sizeof(Kernel::FileInfo), + .st_info = ELF32_ST_INFO(STB_GLOBAL, STT_OBJECT), + .st_other = STV_DEFAULT, + .st_shndx = static_cast(*m_data_index), + }); + + if (inode_number == 2) { + m_generator.symtab().add_symbol( + "__flash_root", Elf32_Sym{ + .st_value = static_cast(inode_offset), + .st_size = sizeof(Kernel::FileInfo), + .st_info = ELF32_ST_INFO(STB_GLOBAL, STT_OBJECT), + .st_other = STV_DEFAULT, + .st_shndx = static_cast(*m_data_index), + }); + } + + for (Elf32_Rel &relocation : inode_relocations) { + relocation.r_offset += inode_offset; + m_data_relocs->add_entry(relocation); + } + + m_inode_to_offset[inode_number] = inode_offset; + + return inode.st_ino; } -void FileSystem::finalize() -{ - assert(!m_finalized); - m_finalized = true; - m_data_relocs->finalize(); - m_generator.write_section(m_data_index.value(), m_data_stream); +void FileSystem::finalize() { + assert(!m_finalized); + m_finalized = true; + + m_data_relocs->finalize(); + m_generator.write_section(m_data_index.value(), m_data_stream); } diff --git a/Tools/FileSystem.hpp b/Tools/FileSystem.hpp index 0340581..dfbd9f1 100644 --- a/Tools/FileSystem.hpp +++ b/Tools/FileSystem.hpp @@ -1,45 +1,58 @@ #pragma once -#include +#include +#include #include +#include +#include +#include +#include #include -#include #include +#include #include class FileSystem { public: - explicit FileSystem(Elf::Generator& generator); - ~FileSystem(); + explicit FileSystem(Elf::Generator &generator); + ~FileSystem(); + + // Add a file from memory buffer to the filesystem + uint32_t + add_file(Elf::MemoryStream &stream, + Kernel::ModeFlags mode = Kernel::ModeFlags::Regular | + Kernel::ModeFlags::DefaultPermissions, + uint32_t inode_number = 0, size_t *load_offset = nullptr); - uint32_t add_file( - Elf::MemoryStream&, - Kernel::ModeFlags mode = Kernel::ModeFlags::Regular | Kernel::ModeFlags::DefaultPermissions, - uint32_t inode_number = 0, - size_t *load_offset = nullptr); + // Add a file from the host filesystem + uint32_t + add_host_file(const std::filesystem::path &path, + Kernel::ModeFlags mode = Kernel::ModeFlags::Regular | + Kernel::ModeFlags::DefaultPermissions); - uint32_t add_host_file( - std::string_view path, - Kernel::ModeFlags mode = Kernel::ModeFlags::Regular | Kernel::ModeFlags::DefaultPermissions); + // Add a directory containing the specified files + uint32_t add_directory(std::map &files, + uint32_t inode_number = 0); - uint32_t add_directory(std::map& files, uint32_t inode_number = 0); - uint32_t add_root_directory(std::map& files); + // Add the root directory + uint32_t add_root_directory(std::map &files); - void finalize(); + // Finalize the filesystem and write section to the ELF generator + void finalize(); private: - Elf::Generator& m_generator; - bool m_finalized = false; + Elf::Generator &m_generator; + bool m_finalized = false; - std::optional m_data_relocs; - std::optional m_data_index; - std::optional m_base_symbol; + std::optional m_data_relocs; + std::optional m_data_index; + std::optional m_base_symbol; - Elf::MemoryStream m_data_stream; + Elf::MemoryStream m_data_stream; - std::map m_inode_to_offset; + std::map m_inode_to_offset; - size_t m_next_inode = 3; + size_t m_next_inode = 3; }; diff --git a/Tools/LibElf/MemoryStream.cpp b/Tools/LibElf/MemoryStream.cpp index f88a47f..a459af7 100644 --- a/Tools/LibElf/MemoryStream.cpp +++ b/Tools/LibElf/MemoryStream.cpp @@ -1,112 +1,133 @@ +#include #include -#include #include -#include +#include #include +#include #include #include "MemoryStream.hpp" -namespace Elf -{ - std::span mmap_file(std::filesystem::path path) - { - int fd = open(path.c_str(), O_RDONLY); - assert(fd >= 0); - - struct stat statbuf; - - int retval = fstat(fd, &statbuf); - assert(retval == 0); - - void *pointer = mmap(nullptr, statbuf.st_size, PROT_READ, MAP_PRIVATE, fd, 0); - assert(pointer != MAP_FAILED); - - return { (const uint8_t*)pointer, (size_t)statbuf.st_size }; - } - MemoryStream::MemoryStream() - { - int fd = memfd_create("MemoryStream", 0); - assert(fd >= 0); - - m_file = fdopen(fd, "w"); - assert(m_file != nullptr); - } - MemoryStream::~MemoryStream() - { - if (m_file) - fclose(m_file); - } - MemoryStream::MemoryStream(MemoryStream&& other) - { - m_file = std::exchange(other.m_file, nullptr); - } - size_t MemoryStream::write_bytes(std::span bytes) - { - size_t offset = this->offset(); - - size_t retval = fwrite(bytes.data(), 1, bytes.size(), m_file); - assert(retval == bytes.size_bytes()); - - return offset; - } - size_t MemoryStream::write_bytes(MemoryStream& other) - { - size_t base_offset = this->offset(); - - size_t offset = other.offset(); - - other.seek(0); - - char buffer[0x1000]; - while (!feof(other.m_file)) { - size_t nread = fread(buffer, 1, sizeof(buffer), other.m_file); - size_t nwritten = fwrite(buffer, 1, nread, m_file); - assert(nread == nwritten); - } - - other.seek(offset); - - return base_offset; - } - size_t MemoryStream::offset() - { - long offset = ftell(m_file); - assert(offset >= 0); - return (size_t)offset; - } - size_t MemoryStream::size() - { - int retval; - - size_t offset = this->offset(); - - retval = fseek(m_file, 0, SEEK_END); - assert(retval == 0); - - size_t size = this->offset(); - - retval = fseek(m_file, offset, SEEK_SET); - assert(retval == 0); - - return size; - } - void MemoryStream::seek(size_t offset) - { - int retval = fseek(m_file, offset, SEEK_SET); - assert(retval == 0); - } - void MemoryStream::seek_relative(ssize_t offset) - { - int retval = fseek(m_file, offset, SEEK_CUR); - assert(retval == 0); - } - void MemoryStream::copy_to_raw_fd(int fd) - { - off_t input_offset = 0; - - ssize_t retval = copy_file_range(fileno(m_file), &input_offset, fd, nullptr, size(), 0); - assert(retval == size()); - } +namespace Elf { +std::span mmap_file(std::filesystem::path path) { + int fd = open(path.c_str(), O_RDONLY); + assert(fd >= 0); + + struct stat statbuf; + + int retval = fstat(fd, &statbuf); + assert(retval == 0); + + void *pointer = mmap(nullptr, statbuf.st_size, PROT_READ, MAP_PRIVATE, fd, 0); + assert(pointer != MAP_FAILED); + + return {(const uint8_t *)pointer, (size_t)statbuf.st_size}; +} +MemoryStream::MemoryStream() { + m_file = tmpfile(); + assert(m_file != nullptr); +} +MemoryStream::~MemoryStream() { + if (m_file) + fclose(m_file); +} +MemoryStream::MemoryStream(MemoryStream &&other) { + m_file = std::exchange(other.m_file, nullptr); +} +size_t MemoryStream::write_bytes(std::span bytes) { + size_t offset = this->offset(); + + size_t retval = fwrite(bytes.data(), 1, bytes.size(), m_file); + assert(retval == bytes.size_bytes()); + + return offset; +} +size_t MemoryStream::write_bytes(MemoryStream &other) { + size_t base_offset = this->offset(); + + size_t offset = other.offset(); + + other.seek(0); + + char buffer[0x1000]; + while (!feof(other.m_file)) { + size_t nread = fread(buffer, 1, sizeof(buffer), other.m_file); + size_t nwritten = fwrite(buffer, 1, nread, m_file); + assert(nread == nwritten); + } + + other.seek(offset); + + return base_offset; +} +size_t MemoryStream::offset() { + long offset = ftell(m_file); + assert(offset >= 0); + return (size_t)offset; +} +size_t MemoryStream::size() { + int retval; + + size_t offset = this->offset(); + + retval = fseek(m_file, 0, SEEK_END); + assert(retval == 0); + + size_t size = this->offset(); + + retval = fseek(m_file, offset, SEEK_SET); + assert(retval == 0); + + return size; +} +void MemoryStream::seek(size_t offset) { + int retval = fseek(m_file, offset, SEEK_SET); + assert(retval == 0); +} +void MemoryStream::seek_relative(ssize_t offset) { + int retval = fseek(m_file, offset, SEEK_CUR); + assert(retval == 0); +} +void MemoryStream::copy_to_raw_fd(int fd) { + size_t current_offset = offset(); + + seek(0); + + char buffer[4096]; + size_t bytes_to_copy = size(); + + while (bytes_to_copy > 0) { + size_t n = std::min(bytes_to_copy, sizeof(buffer)); + size_t nread = fread(buffer, 1, n, m_file); + assert(nread > 0); + + ssize_t nwritten = write(fd, buffer, nread); + assert(nwritten == (ssize_t)nread); + + bytes_to_copy -= nread; + } + + seek(current_offset); +} +void MemoryStream::copy_to_stream(std::ostream &stream) { + size_t current_offset = offset(); + + seek(0); + + char buffer[4096]; + size_t bytes_to_copy = size(); + + while (bytes_to_copy > 0) { + size_t n = std::min(bytes_to_copy, sizeof(buffer)); + size_t nread = fread(buffer, 1, n, m_file); + assert(nread > 0); + + stream.write(buffer, nread); + + bytes_to_copy -= nread; + } + + seek(current_offset); } +} // namespace Elf diff --git a/Tools/LibElf/MemoryStream.hpp b/Tools/LibElf/MemoryStream.hpp index ce0b7f6..2b904e4 100644 --- a/Tools/LibElf/MemoryStream.hpp +++ b/Tools/LibElf/MemoryStream.hpp @@ -1,38 +1,39 @@ #pragma once -#include +#include +#include #include +#include +#include #include -namespace Elf -{ - std::span mmap_file(std::filesystem::path); +namespace Elf { +std::span mmap_file(std::filesystem::path); - class MemoryStream { - public: - MemoryStream(); - ~MemoryStream(); - MemoryStream(MemoryStream&&); +class MemoryStream { +public: + MemoryStream(); + ~MemoryStream(); + MemoryStream(MemoryStream &&); - size_t write_bytes(std::span); - size_t write_bytes(MemoryStream&); + size_t write_bytes(std::span); + size_t write_bytes(MemoryStream &); - template - size_t write_object(const T& value) - { - return write_bytes({ (const uint8_t*)&value, sizeof(value) }); - } + template size_t write_object(const T &value) { + return write_bytes({(const uint8_t *)&value, sizeof(value)}); + } - size_t offset(); - size_t size(); + size_t offset(); + size_t size(); - void seek(size_t); - void seek_relative(ssize_t); + void seek(size_t); + void seek_relative(ssize_t); - void copy_to_raw_fd(int fd); + void copy_to_raw_fd(int fd); + void copy_to_stream(std::ostream &stream); - private: - FILE *m_file; - }; -} +private: + FILE *m_file; +}; +} // namespace Elf diff --git a/Tools/compat/elf.h b/Tools/compat/elf.h new file mode 100644 index 0000000..376979c --- /dev/null +++ b/Tools/compat/elf.h @@ -0,0 +1,116 @@ +#pragma once + +#include + +typedef uint32_t Elf32_Addr; +typedef uint16_t Elf32_Half; +typedef uint32_t Elf32_Off; +typedef int32_t Elf32_Sword; +typedef uint32_t Elf32_Word; + +#define EI_NIDENT 16 + +typedef struct { + unsigned char e_ident[EI_NIDENT]; + Elf32_Half e_type; + Elf32_Half e_machine; + Elf32_Word e_version; + Elf32_Addr e_entry; + Elf32_Off e_phoff; + Elf32_Off e_shoff; + Elf32_Word e_flags; + Elf32_Half e_ehsize; + Elf32_Half e_phentsize; + Elf32_Half e_phnum; + Elf32_Half e_shentsize; + Elf32_Half e_shnum; + Elf32_Half e_shstrndx; +} Elf32_Ehdr; + +typedef struct { + Elf32_Word sh_name; + Elf32_Word sh_type; + Elf32_Word sh_flags; + Elf32_Addr sh_addr; + Elf32_Off sh_offset; + Elf32_Word sh_size; + Elf32_Word sh_link; + Elf32_Word sh_info; + Elf32_Word sh_addralign; + Elf32_Word sh_entsize; +} Elf32_Shdr; + +typedef struct { + Elf32_Word st_name; + Elf32_Addr st_value; + Elf32_Word st_size; + unsigned char st_info; + unsigned char st_other; + Elf32_Half st_shndx; +} Elf32_Sym; + +typedef struct { + Elf32_Addr r_offset; + Elf32_Word r_info; +} Elf32_Rel; + +typedef struct { + Elf32_Addr r_offset; + Elf32_Word r_info; + Elf32_Sword r_addend; +} Elf32_Rela; + +typedef struct { + Elf32_Word p_type; + Elf32_Off p_offset; + Elf32_Addr p_vaddr; + Elf32_Addr p_paddr; + Elf32_Word p_filesz; + Elf32_Word p_memsz; + Elf32_Word p_flags; + Elf32_Word p_align; +} Elf32_Phdr; + +#define EI_CLASS 4 +#define EI_DATA 5 +#define EI_VERSION 6 +#define EI_OSABI 7 +#define EI_ABIVERSION 8 + +#define ELFMAG "\177ELF" +#define SELFMAG 4 + +#define ELFCLASS32 1 +#define ELFDATA2LSB 1 +#define ELFOSABI_NONE 0 + +#define EM_ARM 40 + +#define ET_REL 1 + +#define EV_CURRENT 1 + +#define SHN_UNDEF 0 + +#define SHT_NULL 0 +#define SHT_PROGBITS 1 +#define SHT_SYMTAB 2 +#define SHT_STRTAB 3 +#define SHT_RELA 4 +#define SHT_NOBITS 8 +#define SHT_REL 9 + +#define SHF_WRITE 1 +#define SHF_ALLOC 2 + +#define STB_GLOBAL 1 + +#define STT_OBJECT 1 + +#define STV_DEFAULT 0 + +#define ELF32_ST_INFO(b,t) (((b)<<4)+((t)&0xf)) +#define ELF32_ST_BIND(i) ((i)>>4) +#define ELF32_R_INFO(s,t) ((Elf32_Word)(((s)<<8)+((unsigned char)(t)))) + +#define R_ARM_ABS32 2 diff --git a/Userland/CMakeLists.txt b/Userland/CMakeLists.txt index c739e8c..efe8eab 100644 --- a/Userland/CMakeLists.txt +++ b/Userland/CMakeLists.txt @@ -5,13 +5,11 @@ function(userland_executable name_) add_custom_target(${name_}.1.elf ALL COMMAND clang --target=arm-none-eabi -mcpu=cortex-m0plus -std=gnu11 -Og -g -static -nostdlib -fcolor-diagnostics - -fropi -frwpi -DUSERLAND -I ${CMAKE_CURRENT_SOURCE_DIR}/LibC -I ${CMAKE_SOURCE_DIR} -T ${CMAKE_CURRENT_SOURCE_DIR}/Userland.x -Xlinker --nmagic - --sysroot=/usr/local/arm-none-eabi ${LIBC_SOURCES} ${CMAKE_CURRENT_SOURCE_DIR}/${name_}.c -o ${CMAKE_CURRENT_BINARY_DIR}/${name_}.1.elf) @@ -31,7 +29,7 @@ userland_executable(Example) userland_executable(Editor) add_custom_target(FileSystem.elf ALL - COMMAND ${ELF_EMBED_EXECUTABLE} + COMMAND ${ELF_EMBED_EXECUTABLE} FileSystem.elf Shell.elf Example.elf Editor.elf DEPENDS ${USERLAND_EXECUTABLES} ElfEmbed) add_library(LibEmbeddedFiles OBJECT IMPORTED GLOBAL) diff --git a/Userland/LibC/malloc.c b/Userland/LibC/malloc.c index a4b00c9..ea413c8 100644 --- a/Userland/LibC/malloc.c +++ b/Userland/LibC/malloc.c @@ -1,41 +1,148 @@ -#include -#include #include +#include +#include +#include #include +#include extern char __heap_start__[]; extern char __heap_end__[]; -// FIXME: Implement a proper malloc. +#define MAGIC 0xC0DECAFE + +typedef struct MallocHeader { + struct MallocHeader *next; + struct MallocHeader *prev; + size_t size; // Total size including header + bool free; + uint32_t magic; +} MallocHeader; + +static MallocHeader *head = NULL; -void free(void *pointer) -{ +static void initialize_heap() { + head = (MallocHeader *)__heap_start__; + head->next = NULL; + head->prev = NULL; + head->size = (size_t)(__heap_end__ - __heap_start__); + head->free = true; + head->magic = MAGIC; } -static char *heap; +static size_t align(size_t size) { + if (size % 4 != 0) + return size + 4 - (size % 4); + return size; +} + +void *malloc(size_t size) { + if (size == 0) + return NULL; + + if (head == NULL) { + initialize_heap(); + } + + size_t required_size = align(size) + sizeof(MallocHeader); + MallocHeader *current = head; + + while (current) { + if (current->free && current->size >= required_size) { + // Found a suitable block + // Check if we can split it + if (current->size >= required_size + sizeof(MallocHeader) + 4) { + // Split + MallocHeader *new_block = + (MallocHeader *)((char *)current + required_size); + new_block->size = current->size - required_size; + new_block->free = true; + new_block->magic = MAGIC; + new_block->next = current->next; + new_block->prev = current; -static size_t round_to_word(size_t size) -{ - if (size % 4 != 0) - size = size + 4 - size % 4; - return size; + if (current->next) { + current->next->prev = new_block; + } + current->next = new_block; + current->size = required_size; + } + + current->free = false; + return (void *)(current + 1); + } + current = current->next; + } + + return NULL; // Out of memory +} + +void free(void *pointer) { + if (pointer == NULL) + return; + + MallocHeader *header = (MallocHeader *)pointer - 1; + assert(header->magic == MAGIC); // Corruption check + + header->free = true; + + // Coalesce with next + if (header->next && header->next->free) { + header->size += header->next->size; + header->next = header->next->next; + if (header->next) { + header->next->prev = header; + } + } + + // Coalesce with prev + if (header->prev && header->prev->free) { + header->prev->size += header->size; + header->prev->next = header->next; + if (header->next) { + header->next->prev = header->prev; + } + } } -void* malloc(size_t size) -{ - if (heap == NULL) - heap = __heap_start__; +void *realloc(void *pointer, size_t size) { + if (pointer == NULL) + return malloc(size); + if (size == 0) { + free(pointer); + return NULL; + } + + MallocHeader *header = (MallocHeader *)pointer - 1; + assert(header->magic == MAGIC); + + // Simple implementation: allocate new, copy, free old + void *new_ptr = malloc(size); + if (new_ptr == NULL) + return NULL; - size = round_to_word(size); + size_t copy_size = header->size - sizeof(MallocHeader); + if (size < copy_size) + copy_size = size; - heap += size; + // Manually copy since we don't include string.h for memcpy (avoid dependency + // loop?) Actually malloc.c usually can include string.h + char *src = (char *)pointer; + char *dst = (char *)new_ptr; + for (size_t i = 0; i < copy_size; ++i) { + dst[i] = src[i]; + } - char *pointer = heap - size; - assert(pointer <= __heap_end__); - return pointer; + free(pointer); + return new_ptr; } -void* realloc(void *pointer, size_t size) -{ - return pointer; +void *calloc(size_t nmemb, size_t size) { + size_t total_size = nmemb * size; + void *ptr = malloc(total_size); + if (ptr) { + char *p = (char *)ptr; + for (size_t i = 0; i < total_size; ++i) + p[i] = 0; + } + return ptr; } diff --git a/Userland/LibC/sys/crt0.S b/Userland/LibC/sys/crt0.S index f445bcf..ad8e040 100644 --- a/Userland/LibC/sys/crt0.S +++ b/Userland/LibC/sys/crt0.S @@ -50,3 +50,5 @@ _pc_base: subs r0, r0, r1 bx lr + +.section .note.GNU-stack,"",%progbits diff --git a/Userland/LibC/sys/crt0.c b/Userland/LibC/sys/crt0.c index e2465b3..73c9488 100644 --- a/Userland/LibC/sys/crt0.c +++ b/Userland/LibC/sys/crt0.c @@ -6,18 +6,28 @@ extern uint8_t __bss_end__[]; void rom_functions_init(); +extern void (*__preinit_array_start[])(void); +extern void (*__preinit_array_end[])(void); +extern void (*__init_array_start[])(void); +extern void (*__init_array_end[])(void); +extern void (*__fini_array_start[])(void); +extern void (*__fini_array_end[])(void); + void _init() { rom_functions_init(); memset(__bss_start__, 0, __bss_end__ - __bss_start__); - // FIXME: Call preinit array + for (void (**func)(void) = __preinit_array_start; func < __preinit_array_end; ++func) + (*func)(); - // FIXME: Call init array + for (void (**func)(void) = __init_array_start; func < __init_array_end; ++func) + (*func)(); } void _fini() { - // FIXME: Call fini array + for (void (**func)(void) = __fini_array_start; func < __fini_array_end; ++func) + (*func)(); } diff --git a/Userland/Userland.x b/Userland/Userland.x index 50a97d1..cb1fce0 100644 --- a/Userland/Userland.x +++ b/Userland/Userland.x @@ -1,7 +1,5 @@ ENTRY(_start); -/* FIXME: Keep debugging information! */ - PHDRS { text PT_LOAD; data PT_LOAD; @@ -48,4 +46,13 @@ SECTIONS .stack (NOLOAD) : { . += 0x1100; } :data + + /* DWARF debug sections for GDB */ + .debug_info 0 : { *(.debug_info) } + .debug_abbrev 0 : { *(.debug_abbrev) } + .debug_line 0 : { *(.debug_line) } + .debug_frame 0 : { *(.debug_frame) } + .debug_str 0 : { *(.debug_str) } + .debug_loc 0 : { *(.debug_loc) } + .debug_ranges 0 : { *(.debug_ranges) } } diff --git a/tasks.py b/tasks.py index fd18018..fb95337 100644 --- a/tasks.py +++ b/tasks.py @@ -1,24 +1,23 @@ +""" +Pico-RTOS Development Tasks + +Usage: + inv probe - Start OpenOCD debugger + inv dbg - Start GDB session + inv tty - Connect to serial console + inv backup - Backup repository + inv flash - Flash firmware via UF2 +""" + import invoke import os import tempfile +import glob -@invoke.task -def probe(c, debug=False): - if debug: - debug_flags = "--debug=3" - else: - debug_flags = "" - - c.sudo(f"openocd {debug_flags} -f interface/picoprobe.cfg -f target/rp2040.cfg", pty=True) - -@invoke.task -def dbg(c, gdb="arm-none-eabi-gdb", port=3333): - init_script = tempfile.NamedTemporaryFile(suffix=".gdb") - - # FIXME: This is really ugly. - init_script.write(f"""\ +# GDB initialization script content +GDB_INIT_SCRIPT = """\ target extended-remote localhost:{port} -file Kernel.elf +file build/Kernel.elf define dis_here x/20i ($pc -20) @@ -30,30 +29,87 @@ def dbg(c, gdb="arm-none-eabi-gdb", port=3333): end define rebuild - shell ninja + shell ninja -C build load monitor reset init end set confirm off - set history save on set history size unlimited set history remove-duplicates 1 -""".encode()) - init_script.flush() +""" + + +@invoke.task +def probe(c, debug=False): + """Start OpenOCD with PicoProbe interface.""" + debug_flags = "--debug=3" if debug else "" + c.sudo(f"openocd {debug_flags} -f interface/picoprobe.cfg -f target/rp2040.cfg", pty=True) + + +@invoke.task +def dbg(c, gdb="arm-none-eabi-gdb", port=3333): + """Start GDB session connected to OpenOCD.""" + with tempfile.NamedTemporaryFile(mode='w', suffix=".gdb", delete=False) as init_script: + init_script.write(GDB_INIT_SCRIPT.format(port=port)) + init_script.flush() + c.run(f"{gdb} -q -x {init_script.name}", pty=True) - c.run(f"{gdb} -q -x {init_script.name}", pty=True) @invoke.task def tty(c): - if not os.path.exists("/dev/ttyACM0"): - print("Can not find serial device '/dev/ttyACM0'.") + """Connect to serial console (supports Linux and macOS).""" + # Try common serial device paths + linux_devices = ["/dev/ttyACM0", "/dev/ttyACM1", "/dev/ttyUSB0"] + macos_devices = glob.glob("/dev/cu.usbmodem*") + glob.glob("/dev/tty.usbmodem*") + + device = None + for path in linux_devices + macos_devices: + if os.path.exists(path): + device = path + break + + if device is None: + print("Error: No serial device found.") + print("Checked: /dev/ttyACM*, /dev/ttyUSB*, /dev/cu.usbmodem*, /dev/tty.usbmodem*") + exit(1) + + print(f"Connecting to {device}...") + + if device.startswith("/dev/cu") or device.startswith("/dev/tty."): + # macOS + c.run(f"screen {device} 115200", pty=True) + else: + # Linux + c.sudo(f"stty -F {device} 115200 igncr") + c.sudo(f"tio {device}", pty=True) + + +@invoke.task +def flash(c, uf2_path="build/Kernel.1.uf2"): + """Flash firmware via UF2 (Pico must be in BOOTSEL mode).""" + # Check for mounted Pico + mount_points = ["/Volumes/RPI-RP2", "/media/*/RPI-RP2", "/run/media/*/RPI-RP2"] + + pico_mount = None + for pattern in mount_points: + matches = glob.glob(pattern) + if matches: + pico_mount = matches[0] + break + + if pico_mount is None: + print("Error: Pico not found in BOOTSEL mode.") + print("Hold BOOTSEL while connecting USB, then retry.") exit(1) - c.sudo("stty -F /dev/ttyACM0 115200 igncr") - c.sudo("tio /dev/ttyACM0", pty=True) + print(f"Flashing {uf2_path} to {pico_mount}...") + c.run(f"cp {uf2_path} {pico_mount}/") + print("Done! Pico will reboot automatically.") + @invoke.task def backup(c): + """Backup repository to S3.""" c.run("~/dev/scripts/backup.rb --name 'pico-os' --url 'git@github.com:asynts/os' --upload 's3://backup.asynts.com/git/pico-os'")