From 27802b8ee999354fe20ca4dc834f42421eededf5 Mon Sep 17 00:00:00 2001 From: Berkus Decker Date: Mon, 9 Feb 2026 18:45:37 +0200 Subject: [PATCH 001/107] =?UTF-8?q?feat:=20=F0=9F=93=A6=20Fix=20all=20carg?= =?UTF-8?q?o-make=20commands=20-=20replace=20them=20with=20one=20Justfile?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .cargo/config.toml | 2 +- .github/dependabot.yaml | 7 + .github/dependabot.yml | 21 -- .github/shared/qemu/action.yaml | 43 +++ .github/shared/setup/action.yaml | 30 +++ .github/workflows/build.yaml | 52 ++++ .github/workflows/build.yml | 130 --------- Justfile | 441 +++++++++++++++++++++---------- Makefile.toml | 422 ----------------------------- bin/chainboot/Makefile.toml | 100 ------- bin/chainofcommand/Makefile.toml | 34 --- nucleus/Makefile.toml | 102 ------- 12 files changed, 431 insertions(+), 953 deletions(-) create mode 100644 .github/dependabot.yaml delete mode 100644 .github/dependabot.yml create mode 100644 .github/shared/qemu/action.yaml create mode 100644 .github/shared/setup/action.yaml create mode 100644 .github/workflows/build.yaml delete mode 100644 .github/workflows/build.yml delete mode 100644 Makefile.toml delete mode 100644 bin/chainboot/Makefile.toml delete mode 100644 bin/chainofcommand/Makefile.toml delete mode 100644 nucleus/Makefile.toml diff --git a/.cargo/config.toml b/.cargo/config.toml index fe6d2a7f4..1c6481dcd 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -3,4 +3,4 @@ pipelining = true [target.aarch64-metta-none-eabi] -runner = "cargo make qemu-test-runner" +runner = "just _test-runner" diff --git a/.github/dependabot.yaml b/.github/dependabot.yaml new file mode 100644 index 000000000..cadefb93c --- /dev/null +++ b/.github/dependabot.yaml @@ -0,0 +1,7 @@ +version: 2 +updates: + - package-ecosystem: cargo + directory: "/" + schedule: + interval: daily + open-pull-requests-limit: 10 diff --git a/.github/dependabot.yml b/.github/dependabot.yml deleted file mode 100644 index 20153e95b..000000000 --- a/.github/dependabot.yml +++ /dev/null @@ -1,21 +0,0 @@ -version: 2 -updates: -- package-ecosystem: cargo - directory: "/" - schedule: - interval: daily - open-pull-requests-limit: 10 - ignore: - - dependency-name: cortex-a - versions: - - 5.1.2 - - 5.1.3 - - 5.1.4 - - 5.1.5 - - 5.1.6 - - dependency-name: qemu-exit - versions: - - 1.0.2 - - dependency-name: register - versions: - - 1.0.2 diff --git a/.github/shared/qemu/action.yaml b/.github/shared/qemu/action.yaml new file mode 100644 index 000000000..08a362d30 --- /dev/null +++ b/.github/shared/qemu/action.yaml @@ -0,0 +1,43 @@ +name: "QEMU" +description: "Install QEMU for particular platform" +runs: + using: "composite" + steps: + - name: Install QEMU (Linux) + if: runner.os == 'Linux' + shell: "bash" + run: | + sudo apt-get update + sudo apt-get install --fix-missing qemu-system-aarch64 + + - name: Install QEMU (macOS) + if: runner.os == 'macOS' + shell: "bash" + run: brew install qemu + env: + HOMEBREW_NO_AUTO_UPDATE: 1 + HOMEBREW_NO_BOTTLE_SOURCE_FALLBACK: 1 + HOMEBREW_NO_INSTALL_CLEANUP: 1 + + - name: Install Scoop (Windows) + if: runner.os == 'Windows' + shell: pwsh + run: | + Invoke-WebRequest -UseBasicParsing get.scoop.sh -outfile 'install.ps1' + .\install.ps1 -RunAsAdmin + echo "$HOME\scoop\shims" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append + + - name: Add custom Scoop bucket for QEMU (Windows) + if: runner.os == 'Windows' + shell: pwsh + run: | + scoop bucket add scoop-for-ci https://github.com/metta-systems/scoop-for-ci + + - name: Install QEMU (Windows) + if: runner.os == 'Windows' + shell: pwsh + run: scoop install qemu-1010 + + - name: "Print QEMU Version" + shell: ${{ runner.os == 'Windows' && 'pwsh' || 'bash' }} + run: qemu-system-aarch64 --version diff --git a/.github/shared/setup/action.yaml b/.github/shared/setup/action.yaml new file mode 100644 index 000000000..561a6d7f6 --- /dev/null +++ b/.github/shared/setup/action.yaml @@ -0,0 +1,30 @@ +name: "Prepare" +description: "Prepare Rust build environment and dependencies" +runs: + using: "composite" + steps: + - name: "Install nightly Rust" + uses: actions-rust-lang/setup-rust-toolchain@v1 + with: + rustflags: "" + - name: "Install Just" + uses: "taiki-e/install-action@just" + - name: "Print Rust Version" + shell: ${{ runner.os == 'Windows' && 'pwsh' || 'bash' }} + run: | + rustc -Vv + cargo -Vv + - name: "Install build tools" + shell: ${{ runner.os == 'Windows' && 'pwsh' || 'bash' }} + run: cargo install cargo-binutils + - name: "Validate rust-lld" + if: runner.os == 'macOS' + shell: "bash" + run: | + which rust-lld || echo "Not found" + otool -L ~/.cargo/bin/rust-lld + - name: "Print Tools Versions" + shell: ${{ runner.os == 'Windows' && 'pwsh' || 'bash' }} + run: | + just --version + cargo objcopy --version diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml new file mode 100644 index 000000000..d23b729a9 --- /dev/null +++ b/.github/workflows/build.yaml @@ -0,0 +1,52 @@ +name: Build + +on: + push: + branches: + - "*" + pull_request: + +jobs: + check_formatting: + name: "Check Formatting" + runs-on: ubuntu-latest + timeout-minutes: 2 + steps: + - name: "Checkout Repository" + uses: actions/checkout@v6 + - uses: "./.github/shared/setup" + - run: just fmt-check + + clippy: + name: "Clippy" + needs: check_formatting + strategy: + fail-fast: false + matrix: + platform: [ubuntu-latest, macos-latest, windows-latest] + runs-on: ${{ matrix.platform }} + timeout-minutes: 10 + steps: + - name: "Checkout Repository" + uses: actions/checkout@v6 + - uses: "./.github/shared/setup" + - run: just clippy + + test: + name: Test + needs: check_formatting + strategy: + fail-fast: false + matrix: + platform: [ubuntu-latest, macos-latest, windows-latest] + runs-on: ${{ matrix.platform }} + timeout-minutes: 30 + steps: + - name: "Checkout Repository" + uses: actions/checkout@v6 + - uses: "./.github/shared/setup" + - uses: "./.github/shared/qemu" + - name: "Build kernel" + run: just build + - name: "Run tests" + run: just test diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml deleted file mode 100644 index 5b4998b4f..000000000 --- a/.github/workflows/build.yml +++ /dev/null @@ -1,130 +0,0 @@ -name: Build - -on: - push: - branches: - - "*" - pull_request: - -jobs: - check_formatting: - name: "Check Formatting" - runs-on: ubuntu-latest - timeout-minutes: 2 - steps: - - uses: actions/checkout@v1 - - uses: actions-rust-lang/setup-rust-toolchain@v1 - - run: cargo +nightly fmt -- --check - - clippy: - name: "Clippy" - needs: check_formatting - strategy: - fail-fast: false - matrix: - features: [ - "", - "noserial", - "qemu", - "noserial,qemu", - "jtag", - "noserial,jtag", - # jtag and qemu together don't make much sense - ] - runs-on: ubuntu-latest - timeout-minutes: 10 - steps: - - uses: actions/checkout@v1 - - uses: actions-rust-lang/setup-rust-toolchain@v1 - with: - rustflags: "" - - - run: cargo install cargo-make # TODO cache this tool - - run: env CLIPPY_FEATURES=${{ matrix.features }} cargo make clippy - - test: - name: Test - needs: check_formatting - - strategy: - fail-fast: false - matrix: - platform: [ubuntu-latest, macos-latest, windows-latest] - - runs-on: ${{ matrix.platform }} - timeout-minutes: 30 - - steps: - - name: "Checkout Repository" - uses: actions/checkout@v1 - - - name: "Install nightly Rust" - uses: actions-rust-lang/setup-rust-toolchain@v1 - with: - rustflags: "" - - - name: "Print Rust Version" - run: | - rustc -Vv - cargo -Vv - - - name: "Install build tools" - run: cargo install cargo-make cargo-binutils # TODO cache these tools - - - name: "Validate rust-lld" - if: runner.os == 'macOS' - run: | - which rust-lld || echo "Not found" - otool -L ~/.cargo/bin/rust-lld - - - name: "Print Tools Version" - run: | - cargo make --version - cargo objcopy --version - - - name: "Deny Warnings" - run: cargo make build - env: - RUSTFLAGS: "-D warnings" - - - name: Install QEMU (Linux) - if: runner.os == 'Linux' - run: | - sudo apt-get update - sudo apt-get install --fix-missing qemu-system-aarch64 - - - name: Install QEMU (macOS) - if: runner.os == 'macOS' - run: brew install qemu - env: - HOMEBREW_NO_AUTO_UPDATE: 1 - HOMEBREW_NO_BOTTLE_SOURCE_FALLBACK: 1 - HOMEBREW_NO_INSTALL_CLEANUP: 1 - - - name: Install Scoop (Windows) - if: runner.os == 'Windows' - shell: pwsh - run: | - Invoke-WebRequest -UseBasicParsing get.scoop.sh -outfile 'install.ps1' - .\install.ps1 -RunAsAdmin - echo "$HOME\scoop\shims" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append - - - name: Add custom Scoop bucket for QEMU (Windows) - if: runner.os == 'Windows' - shell: pwsh - run: | - scoop bucket add scoop-for-ci https://github.com/metta-systems/scoop-for-ci - - - name: Install QEMU (Windows) - if: runner.os == 'Windows' - shell: pwsh - run: scoop install qemu-1010 - - - name: "Print QEMU Version" - run: qemu-system-aarch64 --version - - - name: "Build kernel" - run: cargo make build - - - name: "Run tests" - run: cargo make test diff --git a/Justfile b/Justfile index eebe28c35..78507fe13 100644 --- a/Justfile +++ b/Justfile @@ -1,162 +1,340 @@ +# === Configuration === + +target := 'aarch64-metta-none-eabi' +# ⚠️ Target path must be 'escaped' to work on Windows +target_json := "-Zjson-target-spec --target='" + justfile_directory() / 'targets' / target + ".json'" +rust_std := '-Zbuild-std=compiler_builtins,core,alloc -Zbuild-std-features=compiler-builtins-mem' + +# Board presets: rustflags, dtb, qemu-machine +board_rpi3_flags := '-C target-cpu=cortex-a53 --cfg board_rpi3' +board_rpi4_flags := '-C target-cpu=cortex-a73 --cfg board_rpi4' +rpi3_dtb := justfile_directory() / 'targets/bcm2710-rpi-3-b-plus.dtb' +rpi4_dtb := justfile_directory() / 'targets/bcm2711-rpi-4-b.dtb' + +nucleus_link := 'libs/platform/src/raspberrypi/linker/nucleus.ld' +init_link := 'libs/platform/src/raspberrypi/linker/init_thread.ld' +test_link := 'libs/platform/src/raspberrypi/linker/test.ld' +chainboot_link := 'bin/chainboot/src/link.ld' + +fixed_rustflags := '-D warnings -Z macro-backtrace' + +qemu := env('QEMU', 'qemu-system-aarch64') +qemu_machine := env('QEMU_MACHINE', 'raspi3b') +gdb := env('GDB', 'aarch64-elf-gdb') # An aarch64-enabled GDB (brew install aarch64-elf-gdb) +objcopy := 'rust-objcopy' +nm := 'rust-nm' +volume := env('VOLUME', '/Volumes/BOOT') + +kernel_elf := justfile_directory() / 'target' / target / 'release/init_thread' +kernel_bin := justfile_directory() / 'target/kernel.bin' +chainboot_elf := justfile_directory() / 'target' / target / 'release/chainboot' +chainboot_bin := justfile_directory() / 'target/chainboot.bin' + +chainboot_serial := '/dev/tty.SLAB_USBtoUART' +chainboot_baud := '115200' + +# QEMU option fragments +qemu_base_opts := '-M ' + qemu_machine + ' -chardev stdio,mux=on,id=char0,logfile=qemu.log,signal=off -mon chardev=char0 -serial chardev:char0 -semihosting-config enable=on,chardev=char0' +qemu_disasm := '-d in_asm,unimp,int,mmu,cpu_reset,guest_errors,nochain,plugin' +qemu_gdb_opts := '-gdb tcp::5555 -S' +qemu_test_opts := '-nographic' +qemu_disasm_gdb := qemu_disasm + ' ' + qemu_gdb_opts + +gdb_connect := justfile_directory() / 'target' / target / 'gdb-connect' + +openocd_bin := env('OPENOCD', '/usr/local/opt/openocd/4d6519593-rtt/bin/openocd') + +ok_label := '✅' +copy_label := '🔄' + _default: @just --list -make-opts := '--time-summary --hide-uninteresting' -# make-opts := '--quiet' +# === Low-level: cross-compile a single crate === -# Update all dependencies -[group("maintenance")] -deps-up: - cargo update +# Cross-build a crate for a given board with a given linker script and features +[private] +_cross-build crate board='rpi4' linker_script='' features='': + RUSTFLAGS="{{ fixed_rustflags }} {{ if board == 'rpi3' { board_rpi3_flags } else { board_rpi4_flags } }}{{ if linker_script != '' { ' -C link-arg=--script=' + linker_script } else { '' } }}" \ + cargo build {{ target_json }} \ + {{ if features != '' { '--features=' + features } else { '' } }} \ + {{ rust_std }} \ + --release -p {{ crate }} + +# === Kernel (nucleus + init_thread -> kernel.bin) === -# Build default hw kernel and run chainofcommand to boot this kernel onto the board +# Build kernel (features: '' for hw, 'qemu' for emulation) [group("hw")] -boot: chainofcommand - cargo make {{ make-opts }} chainboot # make boot-kernel ? +build board='rpi4' features='': (_cross-build 'nucleus' board nucleus_link features) (_cross-build 'init_thread' board init_link features) + {{ objcopy }} --strip-all -O binary "{{ kernel_elf }}" "{{ kernel_bin }}" + @# TODO: print final binary size! + @echo "{{ok_label}} kernel built for {{ board }}{{ if features != '' { ' [' + features + ']' } else { '' } }}" -# Build and run kernel in QEMU with serial port emulation -[group("emu")] -zellij: - cargo make {{ make-opts }} --makefile $(pwd)/nucleus/Makefile.toml --cwd nucleus zellij-nucleus - zellij --layout emulation/layout.zellij +alias b := build -# Build and run chainboot in QEMU with serial port emulation -[group("emu")] -cb-zellij: - # Connect to it via chainofcommand to load an actual kernel - # TODO: actually run chainofcommand in a zellij session too - cargo make {{ make-opts }} --makefile $(pwd)/bin/chainboot/Makefile.toml --cwd bin/chainboot zellij - zellij --layout emulation/layout.zellij +# === Chainboot === -# Run chainboot with GDB in zellij window -cb-zellij-gdb: - cargo make {{ make-opts }} --makefile $(pwd)/bin/chainboot/Makefile.toml --cwd bin/chainboot zellij-gdb - zellij --layout emulation/layout.zellij +# Build chainboot bootloader (features: '' for hw, 'qemu' for emulation) +[group("hw")] +build-chainboot board='rpi4' features='': (_cross-build 'chainboot' board chainboot_link features) + {{ objcopy }} --strip-all -O binary "{{ chainboot_elf }}" "{{ chainboot_bin }}" + @echo "{{ok_label}} chainboot built for {{ board }}{{ if features != '' { ' [' + features + ']' } else { '' } }}" + +# === Chainofcommand (host tool) === # Build chainofcommand serial loader [group("hw")] chainofcommand: - cargo make {{ make-opts }} --makefile $(pwd)/bin/chainofcommand/Makefile.toml --cwd bin/chainofcommand build + @cargo build -p chainofcommand alias coc := chainofcommand +# === QEMU runners === + # Build and run kernel in QEMU [group("emu")] -qemu: - cargo make {{ make-opts }} --makefile $(pwd)/nucleus/Makefile.toml --cwd nucleus qemu +qemu: (build 'rpi3' 'qemu') + @echo "🚜 Run QEMU {{ qemu_base_opts }} with {{ kernel_bin }}" + @echo "🚜 .. on {{ rpi3_dtb }}" + @rm -f qemu.log + {{ qemu }} {{ qemu_base_opts }} -dtb "{{ rpi3_dtb }}" -kernel "{{ kernel_bin }}" -# Build and run kernel in QEMU with GDB port enabled +# Build and run kernel in QEMU with GDB port [group("emu")] -qemu-gdb: - cargo make {{ make-opts }} --makefile $(pwd)/nucleus/Makefile.toml --cwd nucleus qemu-gdb +qemu-gdb: (build 'rpi3' 'qemu') + @echo "🚜 Run QEMU {{ qemu_base_opts }} {{ qemu_disasm_gdb }} with {{ kernel_bin }}" + @echo "🚜 .. on {{ rpi3_dtb }}" + @rm -f qemu.log + {{ qemu }} {{ qemu_base_opts }} {{ qemu_disasm_gdb }} -dtb "{{ rpi3_dtb }}" -kernel "{{ kernel_bin }}" # Build and run chainboot in QEMU [group("emu")] -cb-qemu: - # Connect to it via chainofcommand to load an actual kernel - cargo make {{ make-opts }} --makefile $(pwd)/bin/chainboot/Makefile.toml --cwd bin/chainboot qemu +cb-qemu: (build-chainboot 'rpi3' 'qemu') + @echo "🚜 Run QEMU {{ qemu_base_opts }} {{ qemu_disasm }} with {{ chainboot_bin }}" + @echo "🚜 .. on {{ rpi3_dtb }}" + @rm -f qemu.log + {{ qemu }} {{ qemu_base_opts }} {{ qemu_disasm }} -serial pty -dtb "{{ rpi3_dtb }}" -kernel "{{ chainboot_bin }}" -# Build and run chainboot in QEMU with GDB port enabled +# Build and run chainboot in QEMU with GDB port [group("emu")] -cb-qemu-gdb: - # Connect to it via chainofcommand to load an actual kernel - cargo make {{ make-opts }} --makefile $(pwd)/bin/chainboot/Makefile.toml --cwd bin/chainboot qemu-gdb +cb-qemu-gdb: (build-chainboot 'rpi3' 'qemu') + @echo "🚜 Run QEMU {{ qemu_base_opts }} {{ qemu_disasm_gdb }} with {{ chainboot_bin }}" + @echo "🚜 .. on {{ rpi3_dtb }}" + @rm -f qemu.log + {{ qemu }} {{ qemu_base_opts }} {{ qemu_disasm_gdb }} -serial pty -dtb "{{ rpi3_dtb }}" -kernel "{{ chainboot_bin }}" -# Build and write kernel to an SD Card +# === Zellij (QEMU in split terminal) === + +[private] +_write-zellij-config bin runner_opts dtb: + #!/usr/bin/env bash + cat > emulation/zellij-config.sh < "{{ gdb_connect }}" < target/{{ MOD }}.dot \ - && dot -Tpng target/{{ MOD }}.dot -o target/{{ MOD }}.png +_gen-deps-graph mod: + cargo modules dependencies --max-depth 5 --no-sysroot --no-externs -p {{ mod }} > target/{{ mod }}.dot \ + && dot -Tpng target/{{ mod }}.dot -o target/{{ mod }}.png # Render modules' usage graph [group("modules")] [macos] -deps-graph MOD: (gen-deps-graph MOD) - open target/{{ MOD }}.png +deps-graph mod: (_gen-deps-graph mod) + open target/{{ mod }}.png # Render modules' usage graph [group("modules")] [windows] -deps-graph MOD: (gen-deps-graph MOD) - start target/{{ MOD }}.png +deps-graph mod: (_gen-deps-graph mod) + start target/{{ mod }}.png # Render modules' usage graph [group("modules")] [linux] -deps-graph MOD: (gen-deps-graph MOD) - xdg-open target/{{ MOD }}.png +deps-graph mod: (_gen-deps-graph mod) + xdg-open target/{{ mod }}.png # Render modules symbol visibility [group("modules")] -exports MOD: - cargo modules structure -p {{ MOD }} +exports mod: + cargo modules structure -p {{ mod }} -# Find orphan files in the module sources +# Find orphan files [group("modules")] -orphans MOD: - cargo modules orphans -p {{ MOD }} - -# Modules dependency visualization end -#============================================================================== - -# Generate and open documentation -[group("maintenance")] -doc: - cargo make {{ make-opts }} docs-flow - -# Check formatting -[group("maintenance")] -fmt-check: - cargo fmt -- --check - -# Run lint tasks -[group("maintenance")] -lint: fmt-check clippy - -# Run pre-push local checks -[group("ci")] -pre-push: fmt-check clippy-pre-push test - -# Run CI tasks -[group("ci")] -ci: clean lint build test +orphans mod: + cargo modules orphans -p {{ mod }} # Prepare local dev tools and set-up git hooks [group("maintenance")] setup-local-dev: - commit-emoji --help || cargo install commit-emoji + which cargo-binstall || cargo install cargo-binstall + commit-emoji --help || cargo binstall -y commit-emoji commit-emoji -i - # Run local shortened clippy before pushing to remote - cp .hooks/pre-push .git/hooks/pre-push + cargo binstall -y cargo-binutils + # todo install rustfilt, what else? + # install pre-push git hook with `just pre-push` diff --git a/Makefile.toml b/Makefile.toml deleted file mode 100644 index 84a9d45b7..000000000 --- a/Makefile.toml +++ /dev/null @@ -1,422 +0,0 @@ -# @todo: replace this with xtask? -# - Kinda tired of the maintenance pains here. - -# -# SPDX-License-Identifier: BlueOak-1.0.0 -# -# Copyright (c) Berkus Decker -# - -# -# Global workspace configuration -# -[config] -min_version = "0.37.0" -default_to_workspace = false -skip_core_tasks = true - -[env] -DEFAULT_TARGET = "aarch64-metta-none-eabi" -# -# === User-configurable === -# -# Pass TARGET env var if it does not match the default target above. -TARGET = { value = "${DEFAULT_TARGET}", condition = { env_not_set = ["TARGET"] } } -# Name of the target board "rpi3" or "rpi4" -# Will be converted by nucleus/build.rs into a board configuration flag like board_rpi3. -TARGET_BOARD = { value = "rpi4", condition = { env_not_set = ["TARGET_BOARD"] } } -# Name of the DTB file for target board configuration, use bcm2710-rpi-3-b-plus.dtb for RasPi3B+ -TARGET_DTB = { value = "${CARGO_MAKE_WORKSPACE_WORKING_DIRECTORY}/targets/bcm2711-rpi-4-b.dtb", condition = { env_not_set = [ - "TARGET_DTB", -] } } -# -# === RUSTFLAGS === -# -FIXED_RUSTFLAGS = "-D warnings -Z macro-backtrace" -BOARD_rpi3_RUSTFLAGS = "-C target-cpu=cortex-a53 --cfg board_rpi3" -BOARD_rpi4_RUSTFLAGS = "-C target-cpu=cortex-a73 --cfg board_rpi4" -# -# === QEMU === -# -# AArch64 QEMU binary -QEMU = { value = "qemu-system-aarch64", condition = { env_not_set = ["QEMU"] } } -# QEMU machine type, defaults to raspi3b but CI runners override it due to ancient QEMU versions they use. -# Supported as of qemu 9.0: -# raspi3ap Raspberry Pi 3A+ (revision 1.0) -# raspi3b Raspberry Pi 3B (revision 1.2) -# raspi4b Raspberry Pi 4B (revision 1.5) -QEMU_MACHINE = { value = "raspi3b", condition = { env_not_set = ["QEMU_MACHINE"] } } - -# An aarch64-enabled GDB -# brew install aarch64-elf-gdb -GDB = { value = "aarch64-elf-gdb", condition = { env_not_set = ["GDB"] } } - -# @todo: Replace it with probe-rs.. which can be installed by `cargo make`! -# OpenOCD with JLink support -# (RTT patch from http://openocd.zylin.com/#/c/4055/11 has already been merged into main line) -OPENOCD = { value = "/usr/local/opt/openocd/4d6519593-rtt/bin/openocd", condition = { env_not_set = [ - "OPENOCD", -] } } -# Mounted sdcard partition path -VOLUME = { value = "/Volumes/BOOT", condition = { env_not_set = ["VOLUME"] } } -# -# === Automatic === -# -# When running `cargo make` for modules which are part of a workspace, you can automatically -# have the member crates makefile (*even if doesn't exist*) extend the workspace level makefile. -# This allows you to maintain a single makefile for the entire workspace but -# have access to those custom tasks in every member crate. -# This is only relevant for workspace builds which are triggered in the workspace root. -# Flows that start directly in the member crate, must manually extend the workspace -# level makefile using the `extend` keyword. -CARGO_MAKE_EXTEND_WORKSPACE_MAKEFILE = true -# Build rust stdlib locally -RUST_STD = "-Zbuild-std=compiler_builtins,core,alloc -Zbuild-std-features=compiler-builtins-mem" -TARGET_JSON = "-Zjson-target-spec --target=${CARGO_MAKE_WORKSPACE_WORKING_DIRECTORY}/targets/${TARGET}.json" -DEVICE_FEATURES = "noserial" -QEMU_FEATURES = "qemu" -TARGET_FEATURES = "" -# Working objcopy from `brew install aarch64-elf-binutils` -# OBJCOPY = "/opt/homebrew/Cellar/aarch64-elf-binutils/2.40/bin/aarch64-elf-objcopy" -# LLVM's objcopy, usually full of bugs like https://github.com/llvm/llvm-project/issues/58407 -OBJCOPY = "rust-objcopy" # Part of `cargo objcopy` in cargo-binutils -OBJCOPY_PARAMS = "--strip-all -O binary" -NM = "rust-nm" # Part of `cargo nm` in cargo-binutils -UTILS_CONTAINER = "andrerichter/raspi3-utils" -DOCKER_CMD = "docker run -it --rm -v ${PWD}:/work -w /work -p 5900:5900" -QEMU_CONTAINER_CMD = "qemu-system-aarch64" -# -# Could additionally use -nographic to disable GUI -- this shall be useful for automated tests. -# -# QEMU has renamed the RasPi machines since version 6.2.0, use just `raspi3` for previous versions. -QEMU_OPTS = "-M ${QEMU_MACHINE} -semihosting" -QEMU_ARM_TRACE_OPTS = "arm_gt_cntvoff_write,arm_gt_ctl_write,arm_gt_cval_write,arm_gt_imask_toggle,arm_gt_recalc,arm_gt_recalc_disabled,arm_gt_tval_write,armsse_cpu_pwrctrl_read,armsse_cpu_pwrctrl_write,armsse_cpuid_read,armsse_cpuid_write,armsse_mhu_read,armsse_mhu_write" -QEMU_BCM_TRACE_OPTS = "bcm2835_cprman_read,bcm2835_cprman_write,bcm2835_cprman_write_invalid_magic,bcm2835_ic_set_cpu_irq,bcm2835_ic_set_gpu_irq,bcm2835_mbox_irq,bcm2835_mbox_property,bcm2835_mbox_read,bcm2835_mbox_write,bcm2835_sdhost_edm_change,bcm2835_sdhost_read,bcm2835_sdhost_update_irq,bcm2835_sdhost_write,bcm2835_systmr_irq_ack,bcm2835_systmr_read,bcm2835_systmr_run,bcm2835_systmr_timer_expired,bcm2835_systmr_write" -QEMU_TRACE_OPTS = "trace:${QEMU_ARM_TRACE_OPTS},${QEMU_BCM_TRACE_OPTS}" # @todo trace: prefix for each opt -QEMU_DISASM_OPTS = "-d in_asm,unimp,int,mmu,cpu_reset,guest_errors,nochain,plugin" -QEMU_SERIAL_OPTS = "-serial stdio -serial pty" -QEMU_TESTS_OPTS = "-nographic" -# For gdb connection: -# - if this is set, MUST have gdb attached for SYS_WRITE0 to work, otherwise QEMU will crash. -# - port 5555 used to match JLink configuration, so we can reuse the same GDB command for both QEMU and JTAG. -QEMU_GDB_OPTS = "-gdb tcp::5555 -S" -GDB_CONNECT_FILE = "${CARGO_MAKE_WORKSPACE_WORKING_DIRECTORY}/target/${TARGET}/gdb-connect" -KERNEL_ELF = "${CARGO_MAKE_WORKSPACE_WORKING_DIRECTORY}/target/${TARGET}/release/nucleus" -KERNEL_BIN = "${CARGO_MAKE_WORKSPACE_WORKING_DIRECTORY}/target/nucleus.bin" -CHAINBOOT_SERIAL = "/dev/tty.SLAB_USBtoUART" -CHAINBOOT_BAUD = 115200 - -# -# === Base reusable commands === -# - -# #Target dependencies: -# [tasks.kernel-binary] -- in nucleus -# alias = "empty" -# -# [tasks.chainboot-binary] -- in bin/chainboot -# alias = "empty" -# -# [tasks.chainofcommand-binary] -- in bin/chainofcommand -# alias = "empty" -# -# [tasks.ttt-binary] -- in tools/ttt -# alias = "empty" - -# By default, build everything. -[tasks.default] -alias = "build" - -# All primary dependencies to build. -[tasks.build] -dependencies = ["x-build-nucleus", "x-build-bin-chainboot", "x-build-bin-chainofcommand"] -# ttt-binary too - -[tasks.x-build-nucleus] -command = "cargo" -# Have to use args to support env vars across platforms, regular scripts don't work on Windows -args = [ - "make", - "--makefile", - "${CARGO_MAKE_WORKING_DIRECTORY}/nucleus/Makefile.toml", - "--cwd", - "nucleus", - "build", -] - -[tasks.x-build-bin-chainboot] -command = "cargo" -# Have to use args to support env vars across platforms, regular scripts don't work on Windows -args = [ - "make", - "--makefile", - "${CARGO_MAKE_WORKING_DIRECTORY}/bin/chainboot/Makefile.toml", - "--cwd", - "bin/chainboot", - "build", -] - -[tasks.x-build-bin-chainofcommand] -command = "cargo" -# Have to use args to support env vars across platforms, regular scripts don't work on Windows -args = [ - "make", - "--makefile", - "${CARGO_MAKE_WORKING_DIRECTORY}/bin/chainofcommand/Makefile.toml", - "--cwd", - "bin/chainofcommand", - "build", -] - -# Run a target build with current platform configuration. -[tasks.build-target] -env = { RUSTFLAGS = "${FIXED_RUSTFLAGS} ${BOARD_RUSTFLAGS} ${TARGET_LINK_RUSTFLAGS}" } -command = "cargo" -args = [ - "build", - "@@split(TARGET_JSON, )", - "--features=${TARGET_FEATURES}", - "@@split(RUST_STD, )", - "--release", -] - -[tasks.build-qemu] -env = { "TARGET_FEATURES" = "${QEMU_FEATURES}", "TARGET_BOARD" = "rpi3", "BOARD_RUSTFLAGS" = "${BOARD_rpi3_RUSTFLAGS}" } -run_task = "build-target" - -[tasks.qemu-runner] -dependencies = ["build-qemu", "convert-kernel-binary"] -env = { "TARGET_FEATURES" = "${QEMU_FEATURES}" } -script = [ - "echo 🚜 Run QEMU ${QEMU_OPTS} ${QEMU_RUNNER_OPTS}", - "echo 🚜 .. with ${KERNEL_BIN}", - "echo 🚜 .. on ${TARGET_DTB}", - "echo", - "echo", - "rm -f qemu.log", - "${QEMU} ${QEMU_OPTS} ${QEMU_RUNNER_OPTS} -dtb \"${TARGET_DTB}\" -kernel \"${KERNEL_BIN}\" 2>&1 | tee qemu.log", - "echo", - "echo", -] - -[tasks.test] -dependencies = ["x-test-nucleus", "x-test-bin-chainboot", "x-test-bin-chainofcommand"] - -[tasks.x-test-nucleus] -command = "cargo" -# Have to use args to support env vars across platforms, regular scripts don't work on Windows -args = [ - "make", - "--makefile", - "${CARGO_MAKE_WORKING_DIRECTORY}/nucleus/Makefile.toml", - "--cwd", - "nucleus", - "test-nucleus", -] - -[tasks.x-test-bin-chainboot] -command = "cargo" -# Have to use args to support env vars across platforms, regular scripts don't work on Windows -args = [ - "make", - "--makefile", - "${CARGO_MAKE_WORKING_DIRECTORY}/bin/chainboot/Makefile.toml", - "--cwd", - "bin/chainboot", - "test-chainboot", -] - -[tasks.x-test-bin-chainofcommand] -command = "cargo" -# Have to use args to support env vars across platforms, regular scripts don't work on Windows -args = [ - "make", - "--makefile", - "${CARGO_MAKE_WORKING_DIRECTORY}/bin/chainofcommand/Makefile.toml", - "--cwd", - "bin/chainofcommand", - "test", -] - -[tasks.test-on-rpi3-qemu] -env = { TARGET_FEATURES = "${QEMU_FEATURES}", TARGET_BOARD = "rpi3", RUSTFLAGS = "${FIXED_RUSTFLAGS} ${BOARD_rpi3_RUSTFLAGS} ${TARGET_LINK_RUSTFLAGS}" } -command = "cargo" -args = [ - "test", - "@@split(TARGET_JSON, )", - "--features=${TARGET_FEATURES}", - "@@split(RUST_STD, )", -] - -# @todo add [tasks.test-on-rpi4-qemu] - -[tasks.docs] -# env = { "TARGET_FEATURES" = "" } -command = "cargo" -args = [ - "doc", - "--open", - "--no-deps", - "@@split(TARGET_JSON, )", - "--features=${TARGET_FEATURES}", -] - -# Run xtool-clippy in all subprojects -[tasks.clippy] -dependencies = ["x-clippy-nucleus", "x-clippy-bin-chainboot", "x-clippy-bin-chainofcommand"] - -[tasks.x-clippy-nucleus] -command = "cargo" -# Have to use args to support env vars across platforms, regular scripts don't work on Windows -args = [ - "make", - "--makefile", - "${CARGO_MAKE_WORKING_DIRECTORY}/nucleus/Makefile.toml", - "--cwd", - "nucleus", - "clippy", -] - -[tasks.x-clippy-bin-chainboot] -command = "cargo" -# Have to use args to support env vars across platforms, regular scripts don't work on Windows -args = [ - "make", - "--makefile", - "${CARGO_MAKE_WORKING_DIRECTORY}/bin/chainboot/Makefile.toml", - "--cwd", - "bin/chainboot", - "clippy", -] - -[tasks.x-clippy-bin-chainofcommand] -command = "cargo" -# Have to use args to support env vars across platforms, regular scripts don't work on Windows -args = [ - "make", - "--makefile", - "${CARGO_MAKE_WORKING_DIRECTORY}/bin/chainofcommand/Makefile.toml", - "--cwd", - "bin/chainofcommand", - "clippy", -] - -# These tasks are written in cargo-make's own script to make them portable across platforms (no `basename` on Windows) - -# #Copy and prepare a given ELF file. Convert to binary output format. -[tasks.convert-custom-binary] -env = { "BINARY_FILE" = "${BINARY_FILE}" } -script_runner = "@duckscript" -script = ''' - binaryFile = basename ${BINARY_FILE} - outElf = set ${CARGO_MAKE_WORKSPACE_WORKING_DIRECTORY}/target/${binaryFile}.elf - outBin = set ${CARGO_MAKE_WORKSPACE_WORKING_DIRECTORY}/target/${binaryFile}.bin - cp ${BINARY_FILE} ${outElf} - exec --fail-on-error ${OBJCOPY} %{OBJCOPY_PARAMS} ${BINARY_FILE} ${outBin} - elfSize = get_file_size ${outElf} - binSize = get_file_size ${outBin} - shortBinary = replace ${BINARY_FILE} "${CARGO_MAKE_WORKSPACE_WORKING_DIRECTORY}/" "" - shortOutElf = replace ${outElf} "${CARGO_MAKE_WORKSPACE_WORKING_DIRECTORY}/" "" - shortOutBin = replace ${outBin} "${CARGO_MAKE_WORKSPACE_WORKING_DIRECTORY}/" "" - echo - echo 🔄 Processing ${shortBinary}: - echo 🔄 Copied ${binaryFile} to ${shortOutElf} (${elfSize} bytes) - echo 💫 Converted ${binaryFile} to ${shortOutBin} (${binSize} bytes) - echo -''' -install_crate = { crate_name = "cargo-binutils", binary = "rust-objcopy", test_arg = [ - "--help", -] } - -# #Copy and prepare binary with tests. -[tasks.test-binary] -env = { "BINARY_FILE" = "${CARGO_MAKE_TASK_ARGS}" } -extend = "convert-custom-binary" - -# #Run binary with tests in QEMU. -# This target is invoked by runner in .cargo/config.toml to run test binary. -[tasks.qemu-test-runner] -dependencies = ["test-binary"] -script_runner = "@duckscript" -script = [ - ''' - binaryFile = basename ${CARGO_MAKE_TASK_ARGS} - echo 🚜 Run QEMU %{QEMU_OPTS} %{QEMU_TESTS_OPTS} - echo 🚜 .. with target/${binaryFile}.bin - # echo 🚜 .. and targets/bcm2710-rpi-3-b-plus.dtb - # echo ${QEMU} %{QEMU_OPTS} %{QEMU_TESTS_OPTS} -dtb ${CARGO_MAKE_WORKSPACE_WORKING_DIRECTORY}/targets/bcm2710-rpi-3-b-plus.dtb -kernel ${CARGO_MAKE_WORKSPACE_WORKING_DIRECTORY}/target/${binaryFile}.bin - echo ${QEMU} %{QEMU_OPTS} %{QEMU_TESTS_OPTS} -kernel ${CARGO_MAKE_WORKSPACE_WORKING_DIRECTORY}/target/${binaryFile}.bin - exec --fail-on-error ${QEMU} %{QEMU_OPTS} %{QEMU_TESTS_OPTS} -dtb ${CARGO_MAKE_WORKSPACE_WORKING_DIRECTORY}/targets/bcm2710-rpi-3-b-plus.dtb -kernel ${CARGO_MAKE_WORKSPACE_WORKING_DIRECTORY}/target/${binaryFile}.bin -''', -] - -# #Generate GDB startup configuration file. -[tasks.generate-gdb-config] -script_runner = "@duckscript" -script = [ - ''' - writefile ${GDB_CONNECT_FILE} "target extended-remote :5555\n" - appendfile ${GDB_CONNECT_FILE} "break *0x80000\n" - appendfile ${GDB_CONNECT_FILE} "break kernel_init\n" - appendfile ${GDB_CONNECT_FILE} "break kernel_main\n" - echo 🖌️ Generated GDB config file -''', -] -# appendfile ${GDB_CONNECT_FILE} "continue\n" - -# #Generate zellij configuration file. -[tasks.generate-zellij-config] -dependencies = ["build-qemu", "kernel-binary"] -script_runner = "@duckscript" -env = { "ZELLIJ_CONFIG_FILE" = "${CARGO_MAKE_WORKSPACE_WORKING_DIRECTORY}/emulation/zellij-config.sh" } -script = [ - ''' - writefile ${ZELLIJ_CONFIG_FILE} "QEMU=\"${QEMU}\"\n" - appendfile ${ZELLIJ_CONFIG_FILE} "QEMU_OPTS=\"${QEMU_OPTS}\"\n" - appendfile ${ZELLIJ_CONFIG_FILE} "QEMU_RUNNER_OPTS=\"${QEMU_RUNNER_OPTS}\"\n" - appendfile ${ZELLIJ_CONFIG_FILE} "CARGO_MAKE_WORKSPACE_WORKING_DIRECTORY=\"${CARGO_MAKE_WORKSPACE_WORKING_DIRECTORY}\"\n" - appendfile ${ZELLIJ_CONFIG_FILE} "TARGET_DTB=\"${TARGET_DTB}\"\n" - appendfile ${ZELLIJ_CONFIG_FILE} "KERNEL_BIN=\"${KERNEL_BIN}\"\n" -''', -] -install_crate = { crate_name = "zellij", binary = "zellij", test_arg = [ - "--help", -] } - -[tasks.openocd] -script = [ - "${OPENOCD} -f interface/jlink.cfg -f ../ocd/${TARGET_BOARD}_target.cfg", -] - -[tasks.sdeject] -dependencies = ["sdcard"] -script = ["diskutil ejectAll ${VOLUME}"] - -[tasks.clean] -command = "cargo" -args = ["clean"] - -# -# === Other commands === -# - -[tasks.xtool-modules] -command = "cargo" -args = ["modules", "tree"] - -[tasks.xtool-expand-target] -# env = { "TARGET_FEATURES" = "" } -command = "cargo" -args = [ - "expand", - "@@split(TARGET_JSON, )", - "--features=${TARGET_FEATURES}", - "--release" -] - -[tasks.xtool-clippy] -env = { RUSTFLAGS = "${FIXED_RUSTFLAGS} ${BOARD_RUSTFLAGS} ${TARGET_LINK_RUSTFLAGS}" } -script = ''' - export RUSTFLAGS="${RUSTFLAGS}" - cargo clippy ${TARGET_JSON} --features=${CLIPPY_FEATURES} ${RUST_STD} -- --deny warnings --allow deprecated -''' diff --git a/bin/chainboot/Makefile.toml b/bin/chainboot/Makefile.toml deleted file mode 100644 index 5b26b7205..000000000 --- a/bin/chainboot/Makefile.toml +++ /dev/null @@ -1,100 +0,0 @@ -# -# SPDX-License-Identifier: BlueOak-1.0.0 -# -# Copyright (c) Berkus Decker -# -# Build chainboot binary -# -extend = "../../Makefile.toml" - -[env] -CHAINBOOT_LINK = "bin/chainboot/src/link.ld" -CHAINBOOT_ELF = "${CARGO_MAKE_WORKSPACE_WORKING_DIRECTORY}/target/${TARGET}/release/chainboot" -CHAINBOOT_BIN = "${CARGO_MAKE_WORKSPACE_WORKING_DIRECTORY}/target/chainboot.bin" - -[tasks.build] -clear = true -dependencies = ["build-target-chainboot", "convert-kernel-binary"] -script_runner = "@duckscript" -script = ''' - shortBinary = replace ${KERNEL_BIN} "${CARGO_MAKE_WORKSPACE_WORKING_DIRECTORY}/" "" - - echo - echo ✅ chainboot built for hw ${TARGET_BOARD} ${BOARD_RUSTFLAGS}. - echo - echo 🎠 Run the following command in your terminal: - echo 🎠 target/debug/chainofcommand ${CHAINBOOT_SERIAL} ${CHAINBOOT_BAUD} --kernel ${shortBinary} - echo -''' - -[tasks.build-target-chainboot] -env = { TARGET_BOARD = "rpi4", TARGET_LINK_RUSTFLAGS = "-C link-arg=--script=${CHAINBOOT_LINK}", BOARD_RUSTFLAGS = "${BOARD_rpi4_RUSTFLAGS}" } -run_task = "build-target" - -# Generate a bootable binary file from CHAINBOOT_ELF -[tasks.convert-kernel-binary] -workspace = false -env = { "BINARY_FILE" = "${CHAINBOOT_ELF}" } -extend = "convert-custom-binary" - -[tasks.test-chainboot] -env = { TARGET_LINK_RUSTFLAGS = "-C link-arg=--script=${CHAINBOOT_LINK}" } -run_task = "test-on-rpi3-qemu" - -[tasks.zellij] -workspace = false -env = { "KERNEL_BIN" = "${CHAINBOOT_BIN}", "QEMU_OPTS" = "${QEMU_OPTS} ${QEMU_DISASM_OPTS}" } -extend = "zellij-config" - -[tasks.zellij-gdb] -workspace = false -env = { "KERNEL_BIN" = "${CHAINBOOT_BIN}", "QEMU_OPTS" = "${QEMU_OPTS} ${QEMU_DISASM_OPTS} ${QEMU_GDB_OPTS}", "TARGET_BOARD" = "rpi3", "TARGET_DTB" = "${CARGO_MAKE_WORKSPACE_WORKING_DIRECTORY}/targets/bcm2710-rpi-3-b-plus.dtb" } -extend = "zellij-config" - -[tasks.qemu] -workspace = false -env = { "QEMU_RUNNER_OPTS" = "${QEMU_DISASM_OPTS} -serial pty", "KERNEL_BIN" = "${CHAINBOOT_BIN}", "TARGET_DTB" = "${CARGO_MAKE_WORKSPACE_WORKING_DIRECTORY}/targets/bcm2710-rpi-3-b-plus.dtb" } -extend = "qemu-runner" - -[tasks.qemu-gdb] -workspace = false -env = { "QEMU_RUNNER_OPTS" = "${QEMU_DISASM_OPTS} ${QEMU_GDB_OPTS} -serial pty", "KERNEL_BIN" = "${CHAINBOOT_BIN}", "TARGET_DTB" = "${CARGO_MAKE_WORKSPACE_WORKING_DIRECTORY}/targets/bcm2710-rpi-3-b-plus.dtb" } -extend = "qemu-runner" - -[tasks.gdb] -workspace = false -dependencies = ["build", "build-kernel-binary", "gdb-config"] -env = { "RUST_GDB" = "${GDB}" } -script = ["exec < /dev/tty && rust-gdb -x ${GDB_CONNECT_FILE} ${CHAINBOOT_ELF}"] - -[tasks.sdcard] -workspace = false -dependencies = ["build", "build-kernel-binary"] -script_runner = "@duckscript" -script = [ - ''' - kernelImage = set "chain_boot_rpi4.img" - cp ${CHAINBOOT_BIN} ${VOLUME}/${kernelImage} - echo 🔄 Copied chainboot to ${VOLUME}/${kernelImage} -''', -] - -# Just use sdeject -# [tasks.cb-eject] -# clean = true -# alias = "cb-eject-chainboot" -# -# [tasks.cb-eject-chainboot] -# dependencies = ["sdeject"] - -[tasks.clippy] -clear = true -dependencies = ["clippy-rpi3", "clippy-rpi4"] - -[tasks.clippy-rpi3] -env = { TARGET_BOARD = "rpi3", TARGET_LINK_RUSTFLAGS = "-C link-arg=--script=${CHAINBOOT_LINK}", BOARD_RUSTFLAGS = "${BOARD_rpi3_RUSTFLAGS}" } -run_task = "xtool-clippy" - -[tasks.clippy-rpi4] -env = { TARGET_BOARD = "rpi4", TARGET_LINK_RUSTFLAGS = "-C link-arg=--script=${CHAINBOOT_LINK}", BOARD_RUSTFLAGS = "${BOARD_rpi4_RUSTFLAGS}" } -run_task = "xtool-clippy" diff --git a/bin/chainofcommand/Makefile.toml b/bin/chainofcommand/Makefile.toml deleted file mode 100644 index 8ea3b7149..000000000 --- a/bin/chainofcommand/Makefile.toml +++ /dev/null @@ -1,34 +0,0 @@ -# -# SPDX-License-Identifier: BlueOak-1.0.0 -# -# Copyright (c) Berkus Decker -# -# Build chainofcommand tool -# -extend = "../../Makefile.toml" - -[tasks.build] -alias = "build-coc" - -[tasks.build-coc] -workspace = false -command = "cargo" -args = ["build"] - -[tasks.test] -clear = true -command = "cargo" -args = ["test"] - -[tasks.clippy] -clear = true -command = "cargo" -args = [ - "clippy", - "--", - "--no-deps", - "--deny", - "warnings", - "--allow", - "deprecated", -] diff --git a/nucleus/Makefile.toml b/nucleus/Makefile.toml deleted file mode 100644 index ccba960af..000000000 --- a/nucleus/Makefile.toml +++ /dev/null @@ -1,102 +0,0 @@ -# -# SPDX-License-Identifier: BlueOak-1.0.0 -# -# Copyright (c) Berkus Decker -# -# Build nucleus -# -extend = "../Makefile.toml" - -[env] -KERNEL_LINK = "libs/platform/src/platform/raspberrypi/linker/kernel.ld" - -[tasks.build] -clear = true -dependencies = ["build-target-nucleus", "convert-kernel-binary"] -script_runner = "@duckscript" -script = ''' - echo - echo ✅ nucleus built for hw ${TARGET_BOARD} ${BOARD_RUSTFLAGS}. - echo -''' - -[tasks.build-target-nucleus] -env = { TARGET_BOARD = "rpi4", TARGET_LINK_RUSTFLAGS = "-C link-arg=--script=${KERNEL_LINK}", BOARD_RUSTFLAGS = "${BOARD_rpi4_RUSTFLAGS}" } -run_task = "build-target" - -# Generate a bootable binary file from KERNEL_ELF -[tasks.convert-kernel-binary] -workspace = false -env = { "BINARY_FILE" = "${KERNEL_ELF}" } -extend = "convert-custom-binary" - -[tasks.test-nucleus] -env = { TARGET_LINK_RUSTFLAGS = "-C link-arg=--script=${KERNEL_LINK}" } -run_task = "test-on-rpi3-qemu" - -[tasks.qemu] -clear = true -alias = "qemu-kernel" - -[tasks.qemu-kernel] -env = { "QEMU_RUNNER_OPTS" = "${QEMU_SERIAL_OPTS}", TARGET_LINK_RUSTFLAGS = "-C link-arg=--script=${KERNEL_LINK}", "TARGET_DTB" = "${CARGO_MAKE_WORKSPACE_WORKING_DIRECTORY}/targets/bcm2710-rpi-3-b-plus.dtb" } -run_task = "qemu-runner" - -[tasks.qemu-gdb] -env = { "QEMU_RUNNER_OPTS" = "${QEMU_SERIAL_OPTS} ${QEMU_DISASM_OPTS} ${QEMU_GDB_OPTS}", "TARGET_DTB" = "${CARGO_MAKE_WORKSPACE_WORKING_DIRECTORY}/targets/bcm2710-rpi-3-b-plus.dtb" } -extend = "qemu-runner" - -[tasks.zellij-nucleus] -alias = "zellij-nucleus-override" - -[tasks.zellij-nucleus-override] -env = { "KERNEL_BIN" = "${KERNEL_BIN}", "QEMU_RUNNER_OPTS" = "${QEMU_SERIAL_OPTS} ${QEMU_DISASM_OPTS} ${QEMU_GDB_OPTS}", "TARGET_DTB" = "${CARGO_MAKE_WORKSPACE_WORKING_DIRECTORY}/targets/bcm2710-rpi-3-b-plus.dtb" } -extend = "zellij-config" - -[tasks.gdb] -dependencies = ["nucleus", "gdb-config"] -env = { "RUST_GDB" = "${GDB}" } -script = ["exec < /dev/tty && pipx run gdbgui -g \"rust-gdb -x ${GDB_CONNECT_FILE} ${KERNEL_ELF}\""] - -[tasks.install-nm] -install_crate = { crate_name = "cargo-binutils", binary = "rust-nm", test_arg = ["--help"] } - -[tasks.install-rustfilt] -install_crate = { crate_name = "rustfilt", binary = "rustfilt", test_arg = ["--help"] } - -[tasks.xtool-nm] -dependencies = ["build", "install-nm", "install-rustfilt"] -script = ["${NM} ${KERNEL_ELF} | sort -k 1 | rustfilt"] - -[tasks.sdcard] # kernel-sdcard ? -dependencies = ["build"] -script_runner = "@duckscript" -script = [ - ''' - kernelImage = set "kernel8.img" - cp ${KERNEL_BIN} ${VOLUME}/${kernelImage} - echo 🔄 Copied nucleus to ${VOLUME}/${kernelImage} -''', -] - -[tasks.hopper] -clear = true -alias = "kernel-hopper" - -[tasks.kernel-hopper] -dependencies = ["build"] -# The cmd line below causes a bug in hopper, see https://www.dropbox.com/s/zyw5mfx0bepcjb1/hopperv4-RAW-bug.mov?dl=0 -# "hopper --loader RAW --base-address 0x80000 --entrypoint 0x80000 --file-offset 0 --plugin arm --cpu aarch64 --variant generic --contains-code true --executable ${KERNEL_BIN}" -script = ["hopper --loader ELF --executable ${KERNEL_ELF}"] - -[tasks.clippy] -clear = true -dependencies = ["clippy-rpi3", "clippy-rpi4"] - -[tasks.clippy-rpi3] -env = { TARGET_BOARD = "rpi3", TARGET_LINK_RUSTFLAGS = "-C link-arg=--script=${KERNEL_LINK}", BOARD_RUSTFLAGS = "${BOARD_rpi3_RUSTFLAGS}" } -run_task = "xtool-clippy" - -[tasks.clippy-rpi4] -env = { TARGET_BOARD = "rpi4", TARGET_LINK_RUSTFLAGS = "-C link-arg=--script=${KERNEL_LINK}", BOARD_RUSTFLAGS = "${BOARD_rpi4_RUSTFLAGS}" } -run_task = "xtool-clippy" From 535d9fc7cf0430d7d1adcb0fbf851f28854cfda6 Mon Sep 17 00:00:00 2001 From: Berkus Decker Date: Sat, 17 Jan 2026 14:14:34 +0200 Subject: [PATCH 002/107] =?UTF-8?q?wip:=20work=20on=20mmu=20setup=20?= =?UTF-8?q?=E2=AC=86=EF=B8=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- nucleus/MMU_Plan.md | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 nucleus/MMU_Plan.md diff --git a/nucleus/MMU_Plan.md b/nucleus/MMU_Plan.md new file mode 100644 index 000000000..e8dc11deb --- /dev/null +++ b/nucleus/MMU_Plan.md @@ -0,0 +1,17 @@ +What's needed: quick-n-dirty higher-half mappings setup and kernel physical memory view setup (both in TTBR1), identity mapping for init_thread (in TTBR0). + +- `__kernel_start` to `__kernel_end` map at KERNEL_HIGH_BASE +- 0 to phys_ram_size (from DTB) map at KERNEL_PHYS_WINDOW +- `__init_thread_start` till `__init_thread_end` identity-map (no code/data split yet?) + +Steps: +- Buildable +- Run steps by step + - Enter kernel init in EL2 - this will be needed to set up kernel mappings + - Print DTB + - Print max RAM from DTB + - Print kernel covered area + - Print KERNEL_HIGH_BASE + - Print kernel mappings size and attribs + - Print init_thread covered area + - Print init_thread mappings size From f9e6a7053bd10ab211cce1e54af20a16e7108c1d Mon Sep 17 00:00:00 2001 From: Berkus Decker Date: Sun, 8 May 2022 22:24:15 +0300 Subject: [PATCH 003/107] wip: better MMU init wip: ignore addresses top byte wip: add CnP docs wip: mmu debug wip: disable old code wip: debug mmu init - more init flags wip: add fixme --- libs/memory/src/arch/aarch64/mmu/mod.rs | 122 +++++- .../src/arch/aarch64/mmu/translation_table.rs | 376 ++++++++++++++++-- 2 files changed, 457 insertions(+), 41 deletions(-) diff --git a/libs/memory/src/arch/aarch64/mmu/mod.rs b/libs/memory/src/arch/aarch64/mmu/mod.rs index de7976ec2..6585b0082 100644 --- a/libs/memory/src/arch/aarch64/mmu/mod.rs +++ b/libs/memory/src/arch/aarch64/mmu/mod.rs @@ -13,13 +13,13 @@ use { crate::{ - Address, Physical, - mmu::{AddressSpace, MMUEnableError, TranslationGranule, interface}, - platform, + mmu::{interface, AddressSpace, MMUEnableError, TranslationGranule}, + platform, Address, Physical, }, aarch64_cpu::{ + asm, asm::barrier, - registers::{ID_AA64MMFR0_EL1, SCTLR_EL1, TCR_EL1, TTBR0_EL1}, + registers::{ID_AA64MMFR0_EL1, SCTLR_EL1, TCR_EL1, TTBR0_EL1, TTBR1_EL1}, }, core::intrinsics::unlikely, liblog::println, @@ -94,19 +94,42 @@ impl MemoryManagementUnit { /// Configure various settings of stage 1 of the EL1 translation regime. fn configure_translation_control() { - let t0sz = (64 - platform::memory::mmu::KernelVirtAddrSpace::SIZE_SHIFT) as u64; + // TCR_EL1.{SH0, ORGN0, IRGN0, SH1, ORGN1, IRGN1} fields define memory region attributes for the + // translation table walk, for each of TTBR0_EL1 and TTBR1_EL1. + // For the Secure and Non-secure EL1&0 stage 1 translations, each of TTBR0_EL1 and TTBR1_EL1 + // contains an ASID field, and the TCR_EL1.A1 field selects which ASID to use. + + // Two-level tables with a 4Kb granule size may address ONLY 1Gb of virtual addresses. + // This seems to be not enough for RPi4? Try using tables from level 1 (TxSZ=below 34 bits), up to 512Gb + + // Configure various settings of stage 1 of the EL1 translation regime. + // PARange is 4 bits, ips is 3 bits @todo validate the range is acceptable. + let ips = ID_AA64MMFR0_EL1.read(ID_AA64MMFR0_EL1::PARange); + + // Maximum 8Gb user VA + let user_va_bits = 33; // ARMv8ARM Table D5-11 minimum TxSZ for starting table level 1 + + // Maximum 8Gb kernel VA + let kernel_va_bits = 33; // ARMv8ARM Table D5-11 minimum TxSZ for starting table level 1 TCR_EL1.write( - TCR_EL1::TBI0::Used - + TCR_EL1::IPS::Bits_40 - + TCR_EL1::TG0::KiB_64 + TCR_EL1::TBI0::Ignored // Top byte ignored, can be used for tagging. + + TCR_EL1::IPS.val(ips) // Intermediate Physical Address Size + // ttbr0 user memory addresses + + TCR_EL1::TG0::KiB_4 // 4 KiB granule + TCR_EL1::SH0::Inner + TCR_EL1::ORGN0::WriteBack_ReadAlloc_WriteAlloc_Cacheable + TCR_EL1::IRGN0::WriteBack_ReadAlloc_WriteAlloc_Cacheable - + TCR_EL1::EPD0::EnableTTBR0Walks - + TCR_EL1::A1::TTBR0 // TTBR0 defines the ASID - + TCR_EL1::T0SZ.val(t0sz) - + TCR_EL1::EPD1::DisableTTBR1Walks, + + TCR_EL1::EPD0::EnableTTBR0Walks, + + TCR_EL1::T0SZ.val(64 - user_va_bits) // ARMv8ARM Table D5-11 minimum TxSZ for starting table level 2 + // ttbr1 kernel memory addresses + + TCR_EL1::TBI1::Ignored // Top byte ignored, can be used for tagging. @todo remove! + + TCR_EL1::TG1::KiB_4 // 4 KiB granule + + TCR_EL1::SH1::Inner + + TCR_EL1::ORGN1::WriteBack_ReadAlloc_WriteAlloc_Cacheable + + TCR_EL1::IRGN1::WriteBack_ReadAlloc_WriteAlloc_Cacheable + + TCR_EL1::EPD1::DisableTTBR1Walks // @fixme disabled for now + + TCR_EL1::T1SZ.val(64 - kernel_va_bits), // ARMv8ARM Table D5-11 minimum TxSZ for starting table level 2 ); } } @@ -148,21 +171,84 @@ impl interface::MMU for MemoryManagementUnit { // .populate_translation_table_entries() // .map_err(|err| MMUEnableError::Other { err })?; - // Set the "Translation Table Base Register". - TTBR0_EL1.set_baddr(phys_tables_base_addr.as_usize() as u64); + // from https://lore.kernel.org/all/db9612a7-9354-2357-9083-1d923b4d11e1@linaro.org/T/ + // The ARMv8.2-TTCNP extension allows an implementation to optimize by + // sharing TLB entries between multiple cores, provided that software + // declares that it's ready to deal with this by setting a CnP bit in + // the TTBRn_ELx. It is mandatory from ARMv8.2 onward. + + // support feature flag is in ID_AA64MMFR2 + // https://developer.arm.com/documentation/ddi0601/2022-03/AArch64-Registers/ID-AA64MMFR2-EL1--AArch64-Memory-Model-Feature-Register-2?lang=en + // CnP bits 3:0 + // From Armv8.2, the only permitted value is 0b0001. + // (this should be set to share the TLBs across cores.) + + // Point to the LVL2 table base address in TTBR0. + TTBR0_EL1.set_baddr(LVL1_TABLE.entries.base_addr_u64()); // User (lo-)space addresses + TTBR0_EL1.modify(TTBR0_EL1::CnP.val(1)); + + // TTBR1_EL1.set_baddr(LVL1_TABLE.entries.base_addr_u64()); // Kernel (hi-)space addresses + // TTBR1_EL1.modify(TTBR1_EL1::CnP.val(1)); + + // upper half, kernel space + // asm volatile ("msr ttbr1_el1, %0" : : "r" ((unsigned long)&_end + TTBR_CNP + PAGESIZE)); Self::configure_translation_control(); // Switch the MMU on. // // First, force all previous changes to be seen before the MMU is enabled. - barrier::isb(barrier::SY); + // See [ARM ARM](https://developer.arm.com/documentation/den0024/a/The-Memory-Management-Unit/The-Translation-Lookaside-Buffer). + barrier::dsb(barrier::ISHST); // ensure write has completed + // core::arch::asm!("tlbi alle1"); // invalidate all TLB entries -- must do it from EL2/EL3 + + barrier::dsb(barrier::ISH); // ensure completion of TLB invalidation + barrier::isb(barrier::SY); // synchronize context and ensure that no instructions are + // fetched using the old translation + + // use cortex_a::regs::RegisterReadWrite; // Enable the MMU and turn on data and instruction caching. - SCTLR_EL1.modify(SCTLR_EL1::M::Enable + SCTLR_EL1::C::Cacheable + SCTLR_EL1::I::Cacheable); - // Force MMU init to complete before next instruction. - barrier::isb(barrier::SY); + SCTLR_EL1.modify( + SCTLR_EL1::EE::LittleEndian // Endianness select in EL1 + + SCTLR_EL1::E0E::LittleEndian // Endianness select in EL0 + + SCTLR_EL1::WXN::Disable // Writable means Execute Never + + SCTLR_EL1::SA::Disable // SP Alignment check in EL1, 16 byte align + + SCTLR_EL1::SA0::Disable // SP Alignment check in EL0, 16 byte align + + SCTLR_EL1::A::Disable // No alignment checks + + SCTLR_EL1::UCI::Trap // Unified Cache instructions trap + + SCTLR_EL1::UCT::Trap // CTR_EL0 instructions trap + + SCTLR_EL1::UMA::Trap // User Mask Access, trap on DAIF access + + SCTLR_EL1::NTWE::Trap // WFE/WFET instruction trap + + SCTLR_EL1::NTWI::Trap // WFI/WFIT instruction trap + + SCTLR_EL1::DZE::Trap // DC ZVA/GVA/GZVA instructions trap + + SCTLR_EL1::C::Cacheable + + SCTLR_EL1::I::Cacheable + + SCTLR_EL1::M::Enable, + ); + + // from https://forums.raspberrypi.com/viewtopic.php?t=320120#p1917769 + // Another hint: once the MMU has been activated you should let 2 CPU cycles pass and then call + // `tlbi alle2` to ensure the MMU related cache will be invalidated and the new settings are picked up. + + asm::nop(); + asm::nop(); + //TODO: tlbi + + // Force MMU init to complete before next instruction + /* + * Invalidate the local I-cache so that any instructions fetched + * speculatively from the PoC are discarded, since they may have + * been dynamically patched at the PoU. + */ + // core::arch::asm!("tlbi alle1"); // invalidate all TLB entries -- must do it from EL2/EL3 + + // FIXME compiler happily inserts an instruction before this one... perhaps a compiler_fence()? + barrier::dsb(barrier::ISH); // ensure completion of TLB invalidation + barrier::isb(barrier::SY); // synchronize context and ensure that no instructions are fetched using the old translation + + println!("MMU activated"); Ok(()) } diff --git a/libs/memory/src/arch/aarch64/mmu/translation_table.rs b/libs/memory/src/arch/aarch64/mmu/translation_table.rs index 2f5562da2..82d42b52c 100644 --- a/libs/memory/src/arch/aarch64/mmu/translation_table.rs +++ b/libs/memory/src/arch/aarch64/mmu/translation_table.rs @@ -1,9 +1,8 @@ use { - super::{Granule64KiB, Granule512MiB, mair}, + super::{mair, Granule512MiB, Granule64KiB}, crate::{ - Address, Physical, Virtual, mmu::{AccessPermissions, AttributeFields, MemAttributes, MemoryRegion, PageAddress}, - platform, + platform, Address, Physical, Virtual, }, core::convert, tock_registers::{ @@ -341,31 +340,78 @@ impl FixedSizeTranslationTable { // OS Interface Code //------------------------------------------------------------------------------ -impl crate::mmu::translation_table::interface::TranslationTable - for FixedSizeTranslationTable -{ +impl FixedSizeTranslationTable { /// Iterates over all static translation table entries and fills them at once. /// + /// See also: https://armv8-ref.codingbelief.com/en/chapter_d4/d4_the_aarch64_virtual_memory_system_archi.html + /// /// # Safety /// /// - Modifies a `static mut`. Ensure it only happens from here. - // pub unsafe fn populate_translation_table_entries(&mut self) -> Result<(), &'static str> { - // for (l2_nr, l2_entry) in self.lvl2.iter_mut().enumerate() { - // *l2_entry = - // TableDescriptor::from_next_lvl_table_addr(self.lvl3[l2_nr].base_addr_usize()); - // - // for (l3_nr, l3_entry) in self.lvl3[l2_nr].iter_mut().enumerate() { - // let virt_addr = (l2_nr << Granule512MiB::SHIFT) + (l3_nr << Granule64KiB::SHIFT); - // - // let (phys_output_addr, attribute_fields) = - // platform::memory::mmu::virt_mem_layout().virt_addr_properties(virt_addr)?; - // - // *l3_entry = PageDescriptor::from_output_addr(phys_output_addr, &attribute_fields); - // } - // } - // - // Ok(()) - // } + pub unsafe fn populate_translation_table_entries(&mut self) -> Result<(), &'static str> { + // Point the first 1GiB to the level 2 table. + // LVL1_TABLE.entries[0] = + // PageTableEntry::new_table_descriptor(LVL2_TABLE.entries.base_addr_usize())?.into(); + // The remaining memory space (1GiB .. 2-8Gib) is yet unmapped. + + // Point the first 2 MiB of virtual addresses to the follow-up LVL3 + // page-table. + // LVL2_TABLE.entries[0] = + // PageTableEntry::new_table_descriptor(LVL3_TABLE.entries.base_addr_usize())?.into(); + + // Fill the rest of the LVL2 (2 MiB) entries as block descriptors. + // + // Notice the skip(1) which makes the iteration start at the second 2 MiB + // block (0x20_0000). + // for (block_descriptor_nr, entry) in LVL2_TABLE.entries.iter_mut().enumerate().skip(1) { + // let virt_addr = block_descriptor_nr << Size2MiB::SHIFT; + // let (output_addr, attribute_fields) = get_virt_addr_properties(virt_addr)?; + // + // println!( + // "Block Descriptor {:4} at {:#010x} -> {:#010x}, {}", + // block_descriptor_nr, virt_addr, output_addr, attribute_fields + // ); + // + // let block_desc = PageTableEntry::new_lvl2_block_descriptor(output_addr, attribute_fields)?; + // *entry = block_desc.into(); + // } + + // Finally, fill the single LVL3 table (4 KiB granule). + // for (page_descriptor_nr, entry) in LVL3_TABLE.entries.iter_mut().enumerate() { + // let virt_addr = page_descriptor_nr << Size4KiB::SHIFT; + // let (output_addr, attribute_fields) = get_virt_addr_properties(virt_addr)?; + // + // println!( + // "Page Descriptor {:4} at {:#010x} -> {:#010x}, {}", + // page_descriptor_nr, virt_addr, output_addr, attribute_fields + // ); + // + // let page_desc = PageTableEntry::new_page_descriptor(output_addr, attribute_fields)?; + // *entry = page_desc.into(); + // } + + for (l2_nr, l2_entry) in self.lvl2.iter_mut().enumerate() { + *l2_entry = + TableDescriptor::from_next_lvl_table_addr(self.lvl3[l2_nr].base_addr_usize()); + + for (l3_nr, l3_entry) in self.lvl3[l2_nr].iter_mut().enumerate() { + let virt_addr = (l2_nr << Granule512MiB::SHIFT) + (l3_nr << Granule64KiB::SHIFT); + + let (phys_output_addr, attribute_fields) = + platform::memory::mmu::unused::virt_mem_layout() + .virt_addr_properties(virt_addr)?; + + *l3_entry = PageDescriptor::from_output_addr(phys_output_addr, &attribute_fields); + } + } + + Ok(()) + } +} + +impl memory::mmu::translation_table::interface::TranslationTable + for FixedSizeTranslationTable +{ fn init(&mut self) -> Result<(), &'static str> { if self.initialized { return Ok(()); @@ -421,3 +467,287 @@ impl crate::mmu::translation_table::interface::Translat Ok(()) } } + +//-------------------------------------------------------------------------------------------------- +// wait: my extended code +//-------------------------------------------------------------------------------------------------- + +/* + * With 4k page granule, a virtual address is split into 4 lookup parts + * spanning 9 bits each: + * + * _______________________________________________ + * | | | | | | | + * | signx | Lv0 | Lv1 | Lv2 | Lv3 | off | + * |_______|_______|_______|_______|_______|_______| + * 63-48 47-39 38-30 29-21 20-12 11-00 + * + * mask page size + * + * Lv0: FF8000000000 -- + * Lv1: 7FC0000000 1G + * Lv2: 3FE00000 2M + * Lv3: 1FF000 4K + * off: FFF + * + * RPi3 supports 64K and 4K granules, also 40-bit physical addresses. + * It also can address only 1G physical memory, so these 40-bit phys addresses are a fake. + * + * 48-bit virtual address space; different mappings in VBAR0 (EL0) and VBAR1 (EL1+). + */ + +/// Number of entries in a 4KiB mmu table. +pub const NUM_ENTRIES_4KIB: u64 = 512; + +/// Trait for abstracting over the possible page sizes, 4KiB, 16KiB, 2MiB, 1GiB. +pub trait PageSize: Copy + Eq + PartialOrd + Ord { + /// The page size in bytes. + const SIZE: u64; + + /// A string representation of the page size for debug output. + const SIZE_AS_DEBUG_STR: &'static str; + + /// The page shift in bits. + const SHIFT: usize; + + /// The page mask in bits. + const MASK: u64; +} + +/// This trait is implemented for 4KiB, 16KiB, and 2MiB pages, but not for 1GiB pages. +pub trait NotGiantPageSize: PageSize {} // @todo doesn't have to be pub?? + +/// A standard 4KiB page. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +pub enum Size4KiB {} + +impl PageSize for Size4KiB { + const SIZE: u64 = 4096; + const SIZE_AS_DEBUG_STR: &'static str = "4KiB"; + const SHIFT: usize = 12; + const MASK: u64 = 0xfff; +} + +impl NotGiantPageSize for Size4KiB {} + +/// A “huge” 2MiB page. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +pub enum Size2MiB {} + +impl PageSize for Size2MiB { + const SIZE: u64 = Size4KiB::SIZE * NUM_ENTRIES_4KIB; + const SIZE_AS_DEBUG_STR: &'static str = "2MiB"; + const SHIFT: usize = 21; + const MASK: u64 = 0x1fffff; +} + +impl NotGiantPageSize for Size2MiB {} + +type EntryFlags = tock_registers::fields::FieldValue; +// type EntryRegister = register::LocalRegisterCopy; + +/// L0 table -- only pointers to L1 tables +pub enum PageGlobalDirectory {} +/// L1 tables -- pointers to L2 tables or giant 1GiB pages +pub enum PageUpperDirectory {} +/// L2 tables -- pointers to L3 tables or huge 2MiB pages +pub enum PageDirectory {} +/// L3 tables -- only pointers to 4/16KiB pages +pub enum PageTable {} + +/// Shared trait for specific table levels. +pub trait TableLevel {} + +/// Shared trait for hierarchical table levels. +/// +/// Specifies what is the next level of page table hierarchy. +pub trait HierarchicalLevel: TableLevel { + /// Level of the next translation table below this one. + type NextLevel: TableLevel; +} + +impl TableLevel for PageGlobalDirectory {} +impl TableLevel for PageUpperDirectory {} +impl TableLevel for PageDirectory {} +impl TableLevel for PageTable {} + +impl HierarchicalLevel for PageGlobalDirectory { + type NextLevel = PageUpperDirectory; +} +impl HierarchicalLevel for PageUpperDirectory { + type NextLevel = PageDirectory; +} +impl HierarchicalLevel for PageDirectory { + type NextLevel = PageTable; +} +// PageTables do not have next level, therefore they are not HierarchicalLevel + +/// MMU address translation table. +/// Contains just u64 internally, provides enum interface on top +#[repr(C)] +#[repr(align(4096))] +pub struct Table { + entries: [u64; NUM_ENTRIES_4KIB as usize], + level: PhantomData, +} + +// Implementation code shared for all levels of page tables +impl Table +where + L: TableLevel, +{ + /// Zero out entire table. + pub fn zero(&mut self) { + for entry in self.entries.iter_mut() { + *entry = 0; + } + } +} + +impl Index for Table +where + L: TableLevel, +{ + type Output = u64; + + fn index(&self, index: usize) -> &u64 { + &self.entries[index] + } +} + +impl IndexMut for Table +where + L: TableLevel, +{ + fn index_mut(&mut self, index: usize) -> &mut u64 { + &mut self.entries[index] + } +} + +/// Type-safe enum wrapper covering Table's 64-bit entries. +#[derive(Clone)] +// #[repr(transparent)] +enum PageTableEntry { + /// Empty page table entry. + Invalid, + /// Table descriptor is a L0, L1 or L2 table pointing to another table. + /// L0 tables can only point to L1 tables. + /// A descriptor pointing to the next page table. + TableDescriptor(EntryFlags), + /// A Level2 block descriptor with 2 MiB aperture. + /// + /// The output points to physical memory. + Lvl2BlockDescriptor(EntryFlags), + /// A page PageTableEntry::descriptor with 4 KiB aperture. + /// + /// The output points to physical memory. + PageDescriptor(EntryFlags), +} + +/// A descriptor pointing to the next page table. (within PageTableEntry enum) +// struct TableDescriptor(register::FieldValue); + +impl PageTableEntry { + fn new_table_descriptor(next_lvl_table_addr: usize) -> Result { + if next_lvl_table_addr % Size4KiB::SIZE as usize != 0 { + // @todo SIZE must be usize + return Err("TableDescriptor: Address is not 4 KiB aligned."); + } + + let shifted = next_lvl_table_addr >> Size4KiB::SHIFT; + + Ok(PageTableEntry::TableDescriptor( + STAGE1_DESCRIPTOR::VALID::True + + STAGE1_DESCRIPTOR::AF::Enabled + + STAGE1_DESCRIPTOR::TYPE::Table + + STAGE1_DESCRIPTOR::NEXT_LVL_TABLE_ADDR_4KiB.val(shifted as u64), + )) + } +} + +/// A Level2 block descriptor with 2 MiB aperture. +/// +/// The output points to physical memory. +// struct Lvl2BlockDescriptor(register::FieldValue); + +impl PageTableEntry { + fn new_lvl2_block_descriptor( + output_addr: usize, + attribute_fields: AttributeFields, + ) -> Result { + if output_addr % Size2MiB::SIZE as usize != 0 { + return Err("BlockDescriptor: Address is not 2 MiB aligned."); + } + + let shifted = output_addr >> Size2MiB::SHIFT; + + Ok(PageTableEntry::Lvl2BlockDescriptor( + STAGE1_DESCRIPTOR::VALID::True + + STAGE1_DESCRIPTOR::AF::Enabled + + STAGE1_DESCRIPTOR::TYPE::Block + + STAGE1_DESCRIPTOR::LVL2_OUTPUT_ADDR_4KiB.val(shifted as u64) + + attribute_fields.into(), + )) + } +} + +/// A page descriptor with 4 KiB aperture. +/// +/// The output points to physical memory. + +impl PageTableEntry { + fn new_page_descriptor( + output_addr: usize, + attribute_fields: AttributeFields, + ) -> Result { + if output_addr % Size4KiB::SIZE as usize != 0 { + return Err("PageDescriptor: Address is not 4 KiB aligned."); + } + + let shifted = output_addr >> Size4KiB::SHIFT; + + Ok(PageTableEntry::PageDescriptor( + STAGE1_DESCRIPTOR::VALID::True + + STAGE1_DESCRIPTOR::AF::Enabled + + STAGE1_DESCRIPTOR::TYPE::Table + + STAGE1_DESCRIPTOR::NEXT_LVL_TABLE_ADDR_4KiB.val(shifted as u64) + + attribute_fields.into(), + )) + } +} + +impl From for PageTableEntry { + fn from(_val: u64) -> PageTableEntry { + // xx00 -> Invalid + // xx10 -> Block Entry in L1 and L2 + // xx11 -> TableDescriptor in L0, L1 and L2 + // xx11 -> PageDescriptor in L3 + PageTableEntry::Invalid + } +} + +impl From for u64 { + fn from(val: PageTableEntry) -> u64 { + match val { + PageTableEntry::Invalid => 0, + PageTableEntry::TableDescriptor(x) + | PageTableEntry::Lvl2BlockDescriptor(x) + | PageTableEntry::PageDescriptor(x) => x.value, + } + } +} + +static mut LVL1_TABLE: Table = Table:: { + entries: [0; NUM_ENTRIES_4KIB as usize], + level: PhantomData, +}; + +static mut LVL2_TABLE: Table = Table:: { + entries: [0; NUM_ENTRIES_4KIB as usize], + level: PhantomData, +}; + +static mut LVL3_TABLE: Table = Table:: { + entries: [0; NUM_ENTRIES_4KIB as usize], + level: PhantomData, +}; From f277ba8e6635b660f421ea809dfe747eec45de0d Mon Sep 17 00:00:00 2001 From: Berkus Decker Date: Sun, 19 Oct 2025 04:52:40 +0300 Subject: [PATCH 004/107] wip: call mmu init from boot code --- libs/boot/Cargo.toml | 1 + libs/boot/src/arch/aarch64/boot.rs | 7 ++++++- libs/memory/src/arch/aarch64/mmu/mod.rs | 12 ++++++------ 3 files changed, 13 insertions(+), 7 deletions(-) diff --git a/libs/boot/Cargo.toml b/libs/boot/Cargo.toml index 9758b10d2..6be6aadf2 100644 --- a/libs/boot/Cargo.toml +++ b/libs/boot/Cargo.toml @@ -23,6 +23,7 @@ aarch64-cpu = { workspace = true } libcpu = { workspace = true } libplatform = { workspace = true } tock-registers = { workspace = true } +# probably libmemory [lints] workspace = true diff --git a/libs/boot/src/arch/aarch64/boot.rs b/libs/boot/src/arch/aarch64/boot.rs index a6eea7fad..670a162d2 100644 --- a/libs/boot/src/arch/aarch64/boot.rs +++ b/libs/boot/src/arch/aarch64/boot.rs @@ -68,7 +68,7 @@ pub unsafe extern "C" fn _startup_in_rust() -> ! { #[cfg(feature = "qemu")] EL3 => setup_and_enter_el1_from_el3(), EL2 => setup_and_enter_el1_from_el2(), - EL1 => reset(), + EL1 => reset(), // Cannot configure memory mappings here properly, fail instead? // if not core0 or not EL3/EL2/EL1, infinitely wait for events _ => endless_sleep(), } @@ -143,6 +143,8 @@ fn setup_and_enter_el1_from_el2() -> ! { + SPSR_EL2::M::EL1h, // Use SP_EL1 ); + // Set up EL1 mmu tables from precomputed KERNEL_TABLES. + // Make the Exception Link Register (EL2) point to reset(). #[allow(clippy::fn_to_numeric_cast_any)] ELR_EL2.set(reset as *const () as u64); @@ -180,6 +182,9 @@ fn setup_and_enter_el1_from_el3() -> ! { + SPSR_EL3::M::EL1h, // Use SP_EL1 ); + // Set up EL1 mmu tables from precomputed KERNEL_TABLES. + // MMU::enable_mmu_and_caching(); + // Make the Exception Link Register (EL3) point to reset(). ELR_EL3.set(reset as *const () as u64); diff --git a/libs/memory/src/arch/aarch64/mmu/mod.rs b/libs/memory/src/arch/aarch64/mmu/mod.rs index 6585b0082..febe09d10 100644 --- a/libs/memory/src/arch/aarch64/mmu/mod.rs +++ b/libs/memory/src/arch/aarch64/mmu/mod.rs @@ -13,12 +13,10 @@ use { crate::{ - mmu::{interface, AddressSpace, MMUEnableError, TranslationGranule}, - platform, Address, Physical, + Address, Physical, mmu::{AddressSpace, MMUEnableError, TranslationGranule, interface}, platform::{self, memory::mmu::KERNEL_TABLES} }, aarch64_cpu::{ - asm, - asm::barrier, + asm::{self, barrier}, registers::{ID_AA64MMFR0_EL1, SCTLR_EL1, TCR_EL1, TTBR0_EL1, TTBR1_EL1}, }, core::intrinsics::unlikely, @@ -184,9 +182,11 @@ impl interface::MMU for MemoryManagementUnit { // (this should be set to share the TLBs across cores.) // Point to the LVL2 table base address in TTBR0. - TTBR0_EL1.set_baddr(LVL1_TABLE.entries.base_addr_u64()); // User (lo-)space addresses + // TODO: USER_TABLES, not KERNEL_TABLES here? + TTBR0_EL1.set_baddr(KERNEL_TABLES.entries.base_addr_u64()); // User (lo-)space addresses TTBR0_EL1.modify(TTBR0_EL1::CnP.val(1)); + // TODO: also do kernel level tables (same mappings but at higher table addresses? need to update ttt to do it) // TTBR1_EL1.set_baddr(LVL1_TABLE.entries.base_addr_u64()); // Kernel (hi-)space addresses // TTBR1_EL1.modify(TTBR1_EL1::CnP.val(1)); @@ -209,7 +209,7 @@ impl interface::MMU for MemoryManagementUnit { // use cortex_a::regs::RegisterReadWrite; // Enable the MMU and turn on data and instruction caching. - +, SCTLR_EL1.modify( SCTLR_EL1::EE::LittleEndian // Endianness select in EL1 + SCTLR_EL1::E0E::LittleEndian // Endianness select in EL0 From 1fd35f13269937f59fb3ca05794dbba39fa4a9b6 Mon Sep 17 00:00:00 2001 From: Berkus Decker Date: Sun, 19 May 2024 00:00:15 +0300 Subject: [PATCH 005/107] =?UTF-8?q?wip:=20=E2=9D=8C=20delete=20me?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit wip: 🚧 play with memory tables separately - this compiles so far wip: 🚧 try impl Index for Stages wip: 🚧 page allocator wip: 🚧 playing with mem tables wip: 🚧 reorder fields wip: 🚧 more playing with mem tables wip: 🚧 continue memtab2 experiment wip: 🚧 continue memtab2 experiment wip: 🚧 conceptually we want something like this wip: 🚧 cleanup, no paste needed wip: 🚧 only output casts and NextStage's Either remain wip: 🚧 compiles wip: 🚧 this almost works wip: 🚧 compiles wip: 🚧 what we want to achieve for construction… wip: 🚧 keep going with memtab2 wip: 🚧 work on indexmut wip: 🚧 memtab work wip mmu plans -- readme doc: add init memory allocation -- readme add paging illustration -- readme this sample pretends that each directory has only 2 table entries. wip move docs [wip] mmu experiments [wip] extract mmu features printer sq extract features [❌almost PICKED] extract phys_frame code [wip] extract virt_page code drop obsolete stuff Start moving code to a new mmu2 module [wip] necessary modifications [wip] add to-kernel-space/from-kernel-space address conversion Implement comparison for invalid virt address error Similar to PhysAddrNotValid. Move PageSize to a mod and implement it for phys frames and virt pages [fixme] move those out [wip] Improve virt_page impl and add tests [wip] memory map initialization [wip] directory levels traversal -- mmu2 Make features printing compile [sq] add missing Clone derives [wip] reshuffle stuff around - to be finalized [sq] use static_assertions [sq] add missing documentation Switch to usize for alignment checks [sq] fix iterator checks [sq] make error enum public [temp] allow dead_code while this code is experimental and unused wip improve mmu mapping code wip mmu wip -- mmu2 sq fix compilation -- mmu2 sq text -- mmu2, readme [wip] memmap [wip] Improve virt_page and virt_addr generic impls Not very good size-wise yet. Probably manual impls for necessary types will be better. [wip] fix some virt_addr/virt_page tests fix pagesize tests Add default() free fn support ❌ adding ttt --- Cargo.toml | 1 + libmemory/memory_test.rs | 100 +++ libs/memory/src/arch/aarch64/README.md | 139 ++++ .../src/arch/aarch64/area_frame_allocator.rs | 106 +++ .../memory/src/arch/aarch64/boot_allocator.rs | 15 + libs/memory/src/arch/aarch64/features.rs | 127 ++++ libs/memory/src/arch/aarch64/mmu/mod.rs | 402 +++++++---- libs/memory/src/arch/aarch64/mmu2.rs | 631 ++++++++++++++++++ .../src/arch/aarch64/mmu_experimental.rs | 279 ++++++++ libs/memory/src/arch/aarch64/mod.rs | 22 +- libs/memory/src/arch/aarch64/page_size.rs | 68 ++ libs/memory/src/arch/aarch64/phys_frame.rs | 202 ++++++ libs/memory/src/arch/aarch64/virt_page.rs | 336 ++++++++++ libs/memory/src/phys_addr.rs | 12 +- .../src/platform/raspberrypi/memory/mmu.rs | 6 +- libs/memory/src/virt_addr.rs | 104 ++- nucleus/Cargo.toml | 1 + nucleus/src/main.rs | 16 +- nucleus/tests/memory.rs | 18 +- play/memtab.rs | 520 +++++++++++++++ play/memtab2.rs | 520 +++++++++++++++ 21 files changed, 3422 insertions(+), 203 deletions(-) create mode 100644 libmemory/memory_test.rs create mode 100644 libs/memory/src/arch/aarch64/area_frame_allocator.rs create mode 100644 libs/memory/src/arch/aarch64/boot_allocator.rs create mode 100644 libs/memory/src/arch/aarch64/features.rs create mode 100644 libs/memory/src/arch/aarch64/mmu2.rs create mode 100644 libs/memory/src/arch/aarch64/mmu_experimental.rs create mode 100644 libs/memory/src/arch/aarch64/page_size.rs create mode 100644 libs/memory/src/arch/aarch64/phys_frame.rs create mode 100644 libs/memory/src/arch/aarch64/virt_page.rs create mode 100644 play/memtab.rs create mode 100644 play/memtab2.rs diff --git a/Cargo.toml b/Cargo.toml index 6854a6661..336fc40cd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -63,6 +63,7 @@ libtime = { path = "libs/time" } once_cell = { version = "1.21", default-features = false, features = ["unstable"] } qemu-exit = { version = "3.0" } snafu = { version = "0.8", default-features = false, features = ["unstable-core-error"] } +static_assertions = { version = "1.1.0" } tock-registers = { version = "0.10" } usize_conversions = { version = "0.2" } ux = { version = "0.1", default-features = false } diff --git a/libmemory/memory_test.rs b/libmemory/memory_test.rs new file mode 100644 index 000000000..240508b7d --- /dev/null +++ b/libmemory/memory_test.rs @@ -0,0 +1,100 @@ +//# anyhow = "*" + +// #![allow(dead_code, unused_variables)] + +// mod arch { pub mod aarch64 { pub mod address { +// use {core::marker::PhantomData, crate::address::{Address, Physical}}; + +// pub struct PhysAddr(Address); + +// impl PhysAddr { +// pub fn new(addr: u64) -> Self { +// Self(Address::::new(addr)) +// } +// } + +// impl Address { +// pub fn new(addr: u64) -> Self { +// Self(addr, PhantomData) +// } +// } +// } } } + +// mod address { +// use core::marker::PhantomData; + +// pub struct Physical; +// pub struct Virtual; +// pub trait AddressType {} +// impl AddressType for Physical {} +// impl AddressType for Virtual {} +// pub struct Address(pub u64, pub PhantomData,); +// } + +// fn main() { +// use arch::aarch64::address::PhysAddr; +// let a = PhysAddr::new(0xf0f0f0f0); +// } + +//================================================================================================= +//================================================================================================= +//================================================================================================= + +#![allow(dead_code, unused_variables)] + +mod arch { + pub mod aarch64 { + pub mod address { + use { + crate::address::{Address, Physical}, + core::marker::PhantomData, + }; + + pub struct Physical; + pub struct Virtual; + + impl AddressType for Physical {} + impl AddressType for Virtual {} + + pub struct PhysAddr(Address); + + impl PhysAddr { + pub fn new(addr: u64) -> Self { + Self(Address::::new(addr)) + } + } + + impl Address { + pub fn new(addr: u64) -> Self { + Self(addr, PhantomData) + } + } + } + } +} + +mod address { + // The platform-independent interface is a union of all the platform-specific + // things, but on the platforms that don't support them they are either errors + // or no-ops. (?? this doesn't sound too strict/safe though ??) + use core::marker::PhantomData; + + pub trait AddressType { + fn new(addr: u64) -> Result; + } + pub struct Address(u64, PhantomData); + impl Address { + pub fn new(addr: u64) -> Result { + let inner = T::new(addr)?; + } + } +} + +fn main() { + use arch::aarch64::address::PhysAddr; // we want to take address::PhysAddr tho - no platform-specific stuff + let a = PhysAddr::new(0xf0f0f0f0); +} + +//================================================================================================= +//================================================================================================= +//================================================================================================= diff --git a/libs/memory/src/arch/aarch64/README.md b/libs/memory/src/arch/aarch64/README.md index 1e4943b11..fcdedda59 100644 --- a/libs/memory/src/arch/aarch64/README.md +++ b/libs/memory/src/arch/aarch64/README.md @@ -8,4 +8,143 @@ a specific-size page. ---- +## Plan + +- 1. MMU tables - because we need separate memspaces for kernel and userspace + - 1a. Allocate initial page tables + - 1b. Map over available RAM sensibly + - 1c. Create kernel's own mapping (TTBR_EL1) + +## What does the kernel MMU code support? + +* mapping +* unmapping +* switching per-process mappings (virtspaces) +* virt2phys resolution +* direct phys access for kernel (TTBR_EL1 mapping to physmem) +* initial kernel memory allocation: for mapping tables and capnodes, for initial thread TCB and stacks + +## public api: + + ARM_MMU invocations: + on page directory cap + cache maintenance (clean/invalidate/unify) + on page table cap + map + unmap + on small frame/frame caps + map + remap + unmap + cache maintenance (clean/invalidate/unify) + get address + on asid control cap + on asid pool cap + + +## Minimum Required Functionality (build from this) + +* resolve VA to PA - resolving lets kernel access mapped process memory. + (start from the process' virtspace root - Page Directory) +* flush page, pd, pt, virtspace - will be important for thread switching +* map a page table to appropriate location +* unmap entire mapped page table +* map a phys frame to virt location +* unmap a mapped frame + + +## Requirements + +``` +GIVEN + A PageGlobalDirectory of process +FIND + A kernel-physical address of where it contains a certain leaf node. +``` + +## sel4 + +> seL4 does not provide virtual memory management, beyond kernel primitives for manipulating hardware paging structures. User-level must provide services for creating intermediate paging structures, mapping and unmapping pages. +> Users are free to define their own address space layout with one restriction: the seL4 kernel claims the high part of the virtual memory range. On most 32-bit platforms, this is 0xe0000000 and above. This variable is set per platform, and can be found by finding the kernelBase variable in the seL4 source. +(from https://docs.sel4.systems/Tutorials/mapping.html) + +> Note that to map a frame multiple times, one must make copies of the frame capability: each frame capability can only track one mapping. + +## howto steps + +initial mapping: +* for kernel space - + 1. obtain memory map + 2. build a list of regions available as RAM + 3. find largest covering page sizes + 4. allocate several memory pages and fill them with table info + (need page table creation functions here) + 5. now kernel is able to address physical memory through it's (negative) kernel mapping. + 6. prepare init thread VSpace + - this is more complicated wrt mapping + +Memory areas: +- map entire RAM through TTBR1 +- need device data map to skip MMIO areas + - read and parse device tree data then.. +- map only init_thread and boot_info structures through TTBR0 + +``` +// The region of the initial thread is the user image + ipcbuf and boot info. + +/* setup virtual memory for the kernel */ ++map_kernel_window(); + +/* Construct an initial address space with enough virtual addresses + * to cover the user image + ipc buffer and bootinfo frames */ +it_pd_cap = create_it_address_space(root_cnode_cap, it_v_reg); + +/* Create and map bootinfo frame cap */ +create_bi_frame_cap( +root_cnode_cap, +it_pd_cap, +bi_frame_pptr, +bi_frame_vptr +); + +/* create the initial thread's IPC buffer */ +ipcbuf_cap = create_ipcbuf_frame(root_cnode_cap, it_pd_cap, ipcbuf_vptr); + +/* create all userland image frames */ +create_frames_ret = + create_frames_of_region( + root_cnode_cap, + it_pd_cap, + ui_reg, + true, + pv_offset + ); +ndks_boot.bi_frame->userImageFrames = create_frames_ret.region; + +... later ... + +/* create the initial thread */ +if (!create_initial_thread( + root_cnode_cap, + it_pd_cap, + v_entry, + bi_frame_vptr, + ipcbuf_vptr, + ipcbuf_cap + )) { + + +/* create all of the untypeds. Both devices and kernel window memory */ +if (!create_untypeds( + root_cnode_cap, +(region_t) { +0xf0000000, (pptr_t)ki_boot_end +} /* reusable boot code/data */ + )) { + return false; +} +``` + +---- + For more information please re-read. diff --git a/libs/memory/src/arch/aarch64/area_frame_allocator.rs b/libs/memory/src/arch/aarch64/area_frame_allocator.rs new file mode 100644 index 000000000..07a3bded4 --- /dev/null +++ b/libs/memory/src/arch/aarch64/area_frame_allocator.rs @@ -0,0 +1,106 @@ +/* + * SPDX-License-Identifier: BlueOak-1.0.0 + */ +use super::{Frame, FrameAllocator}; +use multiboot2::{MemoryArea, MemoryAreaIter}; // replace with DTB? + +pub struct AreaFrameAllocator { + next_free_frame: Frame, + current_area: Option<&'static MemoryArea>, + areas: MemoryAreaIter, + kernel_start: Frame, + kernel_end: Frame, + multiboot_start: Frame, + multiboot_end: Frame, +} + +impl FrameAllocator for AreaFrameAllocator { + fn allocate_frame(&mut self) -> Option { + if let Some(_area) = self.current_area { + // "Clone" the frame to return it if it's free. Frame doesn't + // implement Clone, but we can construct an identical frame. + let frame = Frame { + number: self.next_free_frame.number, + }; + + // the last frame of the current area + let current_area_last_frame = Frame::containing_address(0x3f00_0000); + // { + // let address = area.base_addr + area.length - 1; + // Frame::containing_address(address as usize) + // }; + + if frame > current_area_last_frame { + // all frames of current area are used, switch to next area + // self.choose_next_area(); + unimplemented!(); + } else if frame >= self.kernel_start && frame <= self.kernel_end { + // `frame` is used by the kernel + self.next_free_frame = Frame { + number: self.kernel_end.number + 1, + }; + } else if frame >= self.multiboot_start && frame <= self.multiboot_end { + // `frame` is used by the multiboot information structure + self.next_free_frame = Frame { + number: self.multiboot_end.number + 1, + }; + } else { + // frame is unused, increment `next_free_frame` and return it + self.next_free_frame.number += 1; + return Some(frame); + } + // `frame` was not valid, try it again with the updated `next_free_frame` + self.allocate_frame() + } else { + None // no free frames left + } + } + + fn deallocate_frame(&mut self, _frame: Frame) { + unimplemented!() + } +} + +// Fixme: no multiboot, but dtb instead with avail memory regions +// Need dtb parser here! + +impl AreaFrameAllocator { + pub fn new( + kernel_start: usize, + kernel_end: usize, + multiboot_start: usize, + multiboot_end: usize, + memory_areas: MemoryAreaIter, + ) -> AreaFrameAllocator { + let mut allocator = AreaFrameAllocator { + next_free_frame: Frame::containing_address(0), + current_area: None, + areas: memory_areas, + kernel_start: Frame::containing_address(kernel_start), + kernel_end: Frame::containing_address(kernel_end), + multiboot_start: Frame::containing_address(multiboot_start), + multiboot_end: Frame::containing_address(multiboot_end), + }; + // allocator.choose_next_area(); + allocator.next_free_frame = Frame::containing_address(0x100000); // start from 1Mb + allocator + } + + fn choose_next_area(&mut self) { + self.current_area = self + .areas + .clone() + .filter(|area| { + let address = area.base_addr + area.length - 1; + Frame::containing_address(address as usize) >= self.next_free_frame + }) + .min_by_key(|area| area.base_addr); + + if let Some(area) = self.current_area { + let start_frame = Frame::containing_address(area.base_addr as usize); + if self.next_free_frame < start_frame { + self.next_free_frame = start_frame; + } + } + } +} diff --git a/libs/memory/src/arch/aarch64/boot_allocator.rs b/libs/memory/src/arch/aarch64/boot_allocator.rs new file mode 100644 index 000000000..14b5e6712 --- /dev/null +++ b/libs/memory/src/arch/aarch64/boot_allocator.rs @@ -0,0 +1,15 @@ +/* + * SPDX-License-Identifier: BlueOak-1.0.0 + */ +// Allocate regions from boot memory list obtained from devtree +pub struct BootRegionAllocator {} + +impl BootRegionAllocator { + pub fn new(&boot_info: BootInfo) -> Self { + Self {} + } + + pub fn alloc_region(&mut self) {} + + pub fn alloc_zeroed(&mut self) {} +} diff --git a/libs/memory/src/arch/aarch64/features.rs b/libs/memory/src/arch/aarch64/features.rs new file mode 100644 index 000000000..dfc45ecef --- /dev/null +++ b/libs/memory/src/arch/aarch64/features.rs @@ -0,0 +1,127 @@ +//! Print MMU suported features for debugging. + +use { + crate::println, + cortex_a::registers::{ID_AA64MMFR0_EL1, SCTLR_EL1, TCR_EL1}, + tock_registers::interfaces::Readable, +}; + +/// Parse the ID_AA64MMFR0_EL1 register for runtime information about supported MMU features. +/// Print the current state of TCR register. +pub fn print_features() { + let sctlr = SCTLR_EL1.extract(); + + if let Some(SCTLR_EL1::M::Value::Enable) = sctlr.read_as_enum(SCTLR_EL1::M) { + println!("[i] MMU currently enabled"); + } + + if let Some(SCTLR_EL1::I::Value::Cacheable) = sctlr.read_as_enum(SCTLR_EL1::I) { + println!("[i] MMU I-cache enabled"); + } + + if let Some(SCTLR_EL1::C::Value::Cacheable) = sctlr.read_as_enum(SCTLR_EL1::C) { + println!("[i] MMU D-cache enabled"); + } + + let mmfr = ID_AA64MMFR0_EL1.extract(); + + if let Some(ID_AA64MMFR0_EL1::TGran4::Value::Supported) = + mmfr.read_as_enum(ID_AA64MMFR0_EL1::TGran4) + { + println!("[i] MMU: 4 KiB granule supported!"); + } + + if let Some(ID_AA64MMFR0_EL1::TGran16::Value::Supported) = + mmfr.read_as_enum(ID_AA64MMFR0_EL1::TGran16) + { + println!("[i] MMU: 16 KiB granule supported!"); + } + + if let Some(ID_AA64MMFR0_EL1::TGran64::Value::Supported) = + mmfr.read_as_enum(ID_AA64MMFR0_EL1::TGran64) + { + println!("[i] MMU: 64 KiB granule supported!"); + } + + match mmfr.read_as_enum(ID_AA64MMFR0_EL1::ASIDBits) { + Some(ID_AA64MMFR0_EL1::ASIDBits::Value::Bits_16) => { + println!("[i] MMU: 16 bit ASIDs supported!") + } + Some(ID_AA64MMFR0_EL1::ASIDBits::Value::Bits_8) => { + println!("[i] MMU: 8 bit ASIDs supported!") + } + _ => println!("[i] MMU: Invalid ASID bits specified!"), + } + + match mmfr.read_as_enum(ID_AA64MMFR0_EL1::PARange) { + Some(ID_AA64MMFR0_EL1::PARange::Value::Bits_32) => { + println!("[i] MMU: Up to 32 Bit physical address range supported!") + } + Some(ID_AA64MMFR0_EL1::PARange::Value::Bits_36) => { + println!("[i] MMU: Up to 36 Bit physical address range supported!") + } + Some(ID_AA64MMFR0_EL1::PARange::Value::Bits_40) => { + println!("[i] MMU: Up to 40 Bit physical address range supported!") + } + Some(ID_AA64MMFR0_EL1::PARange::Value::Bits_42) => { + println!("[i] MMU: Up to 42 Bit physical address range supported!") + } + Some(ID_AA64MMFR0_EL1::PARange::Value::Bits_44) => { + println!("[i] MMU: Up to 44 Bit physical address range supported!") + } + Some(ID_AA64MMFR0_EL1::PARange::Value::Bits_48) => { + println!("[i] MMU: Up to 48 Bit physical address range supported!") + } + Some(ID_AA64MMFR0_EL1::PARange::Value::Bits_52) => { + println!("[i] MMU: Up to 52 Bit physical address range supported!") + } + _ => println!("[i] MMU: Invalid PARange specified!"), + } + + let tcr = TCR_EL1.extract(); + + match tcr.read_as_enum(TCR_EL1::IPS) { + Some(TCR_EL1::IPS::Value::Bits_32) => { + println!("[i] MMU: 32 Bit intermediate physical address size supported!") + } + Some(TCR_EL1::IPS::Value::Bits_36) => { + println!("[i] MMU: 36 Bit intermediate physical address size supported!") + } + Some(TCR_EL1::IPS::Value::Bits_40) => { + println!("[i] MMU: 40 Bit intermediate physical address size supported!") + } + Some(TCR_EL1::IPS::Value::Bits_42) => { + println!("[i] MMU: 42 Bit intermediate physical address size supported!") + } + Some(TCR_EL1::IPS::Value::Bits_44) => { + println!("[i] MMU: 44 Bit intermediate physical address size supported!") + } + Some(TCR_EL1::IPS::Value::Bits_48) => { + println!("[i] MMU: 48 Bit intermediate physical address size supported!") + } + Some(TCR_EL1::IPS::Value::Bits_52) => { + println!("[i] MMU: 52 Bit intermediate physical address size supported!") + } + _ => println!("[i] MMU: Invalid IPS specified!"), + } + + match tcr.read_as_enum(TCR_EL1::TG0) { + Some(TCR_EL1::TG0::Value::KiB_4) => println!("[i] MMU: TTBR0 4 KiB granule active!"), + Some(TCR_EL1::TG0::Value::KiB_16) => println!("[i] MMU: TTBR0 16 KiB granule active!"), + Some(TCR_EL1::TG0::Value::KiB_64) => println!("[i] MMU: TTBR0 64 KiB granule active!"), + _ => println!("[i] MMU: Invalid TTBR0 granule size specified!"), + } + + let t0sz = tcr.read(TCR_EL1::T0SZ); + println!("[i] MMU: T0sz = 64-{} = {} bits", t0sz, 64 - t0sz); + + match tcr.read_as_enum(TCR_EL1::TG1) { + Some(TCR_EL1::TG1::Value::KiB_4) => println!("[i] MMU: TTBR1 4 KiB granule active!"), + Some(TCR_EL1::TG1::Value::KiB_16) => println!("[i] MMU: TTBR1 16 KiB granule active!"), + Some(TCR_EL1::TG1::Value::KiB_64) => println!("[i] MMU: TTBR1 64 KiB granule active!"), + _ => println!("[i] MMU: Invalid TTBR1 granule size specified!"), + } + + let t1sz = tcr.read(TCR_EL1::T1SZ); + println!("[i] MMU: T1sz = 64-{} = {} bits", t1sz, 64 - t1sz); +} diff --git a/libs/memory/src/arch/aarch64/mmu/mod.rs b/libs/memory/src/arch/aarch64/mmu/mod.rs index febe09d10..101b167e8 100644 --- a/libs/memory/src/arch/aarch64/mmu/mod.rs +++ b/libs/memory/src/arch/aarch64/mmu/mod.rs @@ -1,16 +1,3 @@ -/* - * SPDX-License-Identifier: MIT OR BlueOak-1.0.0 - * Copyright (c) 2018-2019 Andre Richter - * Copyright (c) Berkus Decker - * Original code distributed under MIT, additional changes are under BlueOak-1.0.0 - */ - -//! MMU initialisation. -//! -//! Paging is mostly based on [previous version](https://os.phil-opp.com/page-tables/) of -//! Phil Opp's [paging guide](https://os.phil-opp.com/paging-implementation/) and -//! [ARMv8 ARM memory addressing](https://static.docs.arm.com/100940/0100/armv8_a_address%20translation_100940_0100_en.pdf). - use { crate::{ Address, Physical, mmu::{AddressSpace, MMUEnableError, TranslationGranule, interface}, platform::{self, memory::mmu::KERNEL_TABLES} @@ -20,11 +7,10 @@ use { registers::{ID_AA64MMFR0_EL1, SCTLR_EL1, TCR_EL1, TTBR0_EL1, TTBR1_EL1}, }, core::intrinsics::unlikely, - liblog::println, tock_registers::interfaces::{ReadWriteable, Readable, Writeable}, }; -pub mod translation_table; +pub(crate) mod translation_table; //-------------------------------------------------------------------------------------------------- // Private Definitions @@ -40,7 +26,7 @@ struct MemoryManagementUnit; pub type Granule512MiB = TranslationGranule<{ 512 * 1024 * 1024 }>; pub type Granule64KiB = TranslationGranule<{ 64 * 1024 }>; -/// Constants for indexing the `MAIR_EL1`. +/// Constants for indexing the MAIR_EL1. #[allow(dead_code)] pub mod mair { // Three descriptive consts for indexing into the correct MAIR_EL1 attributes. @@ -48,6 +34,8 @@ pub mod mair { pub const NORMAL: u64 = 0; pub const NORMAL_NON_CACHEABLE: u64 = 1; pub const DEVICE_NGNRE: u64 = 2; + // DEVICE_GRE + // DEVICE_NGNRNE } } @@ -74,8 +62,8 @@ impl AddressSpace { } impl MemoryManagementUnit { - /// Setup function for the `MAIR_EL1` register. - fn set_up_mair() { + /// Setup function for the MAIR_EL1 register. + fn set_up_mair(&self) { use aarch64_cpu::registers::MAIR_EL1; // Define the three memory types that we will map: Normal DRAM, Uncached and device. MAIR_EL1.write( @@ -91,7 +79,7 @@ impl MemoryManagementUnit { } /// Configure various settings of stage 1 of the EL1 translation regime. - fn configure_translation_control() { + fn configure_translation_control(&self) { // TCR_EL1.{SH0, ORGN0, IRGN0, SH1, ORGN1, IRGN1} fields define memory region attributes for the // translation table walk, for each of TTBR0_EL1 and TTBR1_EL1. // For the Secure and Non-secure EL1&0 stage 1 translations, each of TTBR0_EL1 and TTBR1_EL1 @@ -118,7 +106,7 @@ impl MemoryManagementUnit { + TCR_EL1::SH0::Inner + TCR_EL1::ORGN0::WriteBack_ReadAlloc_WriteAlloc_Cacheable + TCR_EL1::IRGN0::WriteBack_ReadAlloc_WriteAlloc_Cacheable - + TCR_EL1::EPD0::EnableTTBR0Walks, + + TCR_EL1::EPD0::EnableTTBR0Walks + TCR_EL1::T0SZ.val(64 - user_va_bits) // ARMv8ARM Table D5-11 minimum TxSZ for starting table level 2 // ttbr1 kernel memory addresses + TCR_EL1::TBI1::Ignored // Top byte ignored, can be used for tagging. @todo remove! @@ -162,7 +150,7 @@ impl interface::MMU for MemoryManagementUnit { } // Prepare the memory attribute indirection register. - Self::set_up_mair(); + self.set_up_mair(); // // Populate translation tables. // KERNEL_TABLES @@ -193,7 +181,7 @@ impl interface::MMU for MemoryManagementUnit { // upper half, kernel space // asm volatile ("msr ttbr1_el1, %0" : : "r" ((unsigned long)&_end + TTBR_CNP + PAGESIZE)); - Self::configure_translation_control(); + self.configure_translation_control(); // Switch the MMU on. // @@ -257,125 +245,293 @@ impl interface::MMU for MemoryManagementUnit { fn is_enabled(&self) -> bool { SCTLR_EL1.matches_all(SCTLR_EL1::M::Enable) } +} - /// Parse the `ID_AA64MMFR0_EL1` register for runtime information about supported MMU features. - /// Print the current state of TCR register. - fn print_features(&self) { - // use crate::cortex_a::regs::RegisterReadWrite; - let sctlr = SCTLR_EL1.extract(); +/// Type-safe enum wrapper covering Table's 64-bit entries. +#[derive(Clone)] +// #[repr(transparent)] +enum PageTableEntry { + /// Empty page table entry. + Invalid, + /// Table descriptor is a L0, L1 or L2 table pointing to another table. + /// L0 tables can only point to L1 tables. + /// A descriptor pointing to the next page table. + TableDescriptor(EntryFlags), + /// A Level2 block descriptor with 2 MiB aperture. + /// + /// The output points to physical memory. + Lvl2BlockDescriptor(EntryFlags), + /// A page PageTableEntry::descriptor with 4 KiB aperture. + /// + /// The output points to physical memory. + PageDescriptor(EntryFlags), +} - if let Some(SCTLR_EL1::M::Value::Enable) = sctlr.read_as_enum(SCTLR_EL1::M) { - println!("[i] MMU currently enabled"); - } +/// A descriptor pointing to the next page table. (within PageTableEntry enum) +// struct TableDescriptor(register::FieldValue); - if let Some(SCTLR_EL1::I::Value::Cacheable) = sctlr.read_as_enum(SCTLR_EL1::I) { - println!("[i] MMU I-cache enabled"); +impl PageTableEntry { + fn new_table_descriptor(next_lvl_table_addr: usize) -> Result { + if next_lvl_table_addr % Size4KiB::SIZE as usize != 0 { + // @todo SIZE must be usize + return Err("TableDescriptor: Address is not 4 KiB aligned."); } - if let Some(SCTLR_EL1::C::Value::Cacheable) = sctlr.read_as_enum(SCTLR_EL1::C) { - println!("[i] MMU D-cache enabled"); - } + let shifted = next_lvl_table_addr >> Size4KiB::SHIFT; - let mmfr = ID_AA64MMFR0_EL1.extract(); + Ok(PageTableEntry::TableDescriptor( + STAGE1_DESCRIPTOR::VALID::True + + STAGE1_DESCRIPTOR::TYPE::Table + + STAGE1_DESCRIPTOR::NEXT_LVL_TABLE_ADDR_4KiB.val(shifted as u64), + )) + } +} - if let Some(ID_AA64MMFR0_EL1::TGran4::Value::Supported) = - mmfr.read_as_enum(ID_AA64MMFR0_EL1::TGran4) - { - println!("[i] MMU: 4 KiB granule supported!"); - } +#[derive(Snafu, Debug)] +enum PageTableError { + #[snafu(display("BlockDescriptor: Address is not 2 MiB aligned."))] + //"PageDescriptor: Address is not 4 KiB aligned." + NotAligned(&'static str), +} - if let Some(ID_AA64MMFR0_EL1::TGran16::Value::Supported) = - mmfr.read_as_enum(ID_AA64MMFR0_EL1::TGran16) - { - println!("[i] MMU: 16 KiB granule supported!"); +/// A Level2 block descriptor with 2 MiB aperture. +/// +/// The output points to physical memory. +// struct Lvl2BlockDescriptor(register::FieldValue); + +impl PageTableEntry { + fn new_lvl2_block_descriptor( + output_addr: usize, + attribute_fields: AttributeFields, + ) -> Result { + if output_addr % Size2MiB::SIZE as usize != 0 { + return Err(PageTableError::NotAligned(Size2MiB::SIZE_AS_DEBUG_STR)); } - if let Some(ID_AA64MMFR0_EL1::TGran64::Value::Supported) = - mmfr.read_as_enum(ID_AA64MMFR0_EL1::TGran64) - { - println!("[i] MMU: 64 KiB granule supported!"); - } + let shifted = output_addr >> Size2MiB::SHIFT; - match mmfr.read_as_enum(ID_AA64MMFR0_EL1::ASIDBits) { - Some(ID_AA64MMFR0_EL1::ASIDBits::Value::Bits_16) => { - println!("[i] MMU: 16 bit ASIDs supported!"); - } - Some(ID_AA64MMFR0_EL1::ASIDBits::Value::Bits_8) => { - println!("[i] MMU: 8 bit ASIDs supported!"); - } - _ => println!("[i] MMU: Invalid ASID bits specified!"), - } + Ok(PageTableEntry::Lvl2BlockDescriptor( + STAGE1_DESCRIPTOR::VALID::True + + STAGE1_DESCRIPTOR::AF::True + + into_mmu_attributes(attribute_fields) + + STAGE1_DESCRIPTOR::TYPE::Block + + STAGE1_DESCRIPTOR::LVL2_OUTPUT_ADDR_4KiB.val(shifted as u64), + )) + } +} - match mmfr.read_as_enum(ID_AA64MMFR0_EL1::PARange) { - Some(ID_AA64MMFR0_EL1::PARange::Value::Bits_32) => { - println!("[i] MMU: Up to 32 Bit physical address range supported!"); - } - Some(ID_AA64MMFR0_EL1::PARange::Value::Bits_36) => { - println!("[i] MMU: Up to 36 Bit physical address range supported!"); - } - Some(ID_AA64MMFR0_EL1::PARange::Value::Bits_40) => { - println!("[i] MMU: Up to 40 Bit physical address range supported!"); - } - Some(ID_AA64MMFR0_EL1::PARange::Value::Bits_42) => { - println!("[i] MMU: Up to 42 Bit physical address range supported!"); - } - Some(ID_AA64MMFR0_EL1::PARange::Value::Bits_44) => { - println!("[i] MMU: Up to 44 Bit physical address range supported!"); - } - Some(ID_AA64MMFR0_EL1::PARange::Value::Bits_48) => { - println!("[i] MMU: Up to 48 Bit physical address range supported!"); - } - Some(ID_AA64MMFR0_EL1::PARange::Value::Bits_52) => { - println!("[i] MMU: Up to 52 Bit physical address range supported!"); - } - _ => println!("[i] MMU: Invalid PARange specified!"), +/// A page descriptor with 4 KiB aperture. +/// +/// The output points to physical memory. + +impl PageTableEntry { + fn new_page_descriptor( + output_addr: usize, + attribute_fields: AttributeFields, + ) -> Result { + if output_addr % Size4KiB::SIZE as usize != 0 { + return Err(PageTableError::NotAligned(Size4KiB::SIZE_AS_DEBUG_STR)); } - let tcr = TCR_EL1.extract(); - - match tcr.read_as_enum(TCR_EL1::IPS) { - Some(TCR_EL1::IPS::Value::Bits_32) => { - println!("[i] MMU: 32 Bit intermediate physical address size supported!"); - } - Some(TCR_EL1::IPS::Value::Bits_36) => { - println!("[i] MMU: 36 Bit intermediate physical address size supported!"); - } - Some(TCR_EL1::IPS::Value::Bits_40) => { - println!("[i] MMU: 40 Bit intermediate physical address size supported!"); - } - Some(TCR_EL1::IPS::Value::Bits_42) => { - println!("[i] MMU: 42 Bit intermediate physical address size supported!"); - } - Some(TCR_EL1::IPS::Value::Bits_44) => { - println!("[i] MMU: 44 Bit intermediate physical address size supported!"); - } - Some(TCR_EL1::IPS::Value::Bits_48) => { - println!("[i] MMU: 48 Bit intermediate physical address size supported!"); - } - Some(TCR_EL1::IPS::Value::Bits_52) => { - println!("[i] MMU: 52 Bit intermediate physical address size supported!"); - } - _ => println!("[i] MMU: Invalid IPS specified!"), - } + let shifted = output_addr >> Size4KiB::SHIFT; - match tcr.read_as_enum(TCR_EL1::TG0) { - Some(TCR_EL1::TG0::Value::KiB_4) => println!("[i] MMU: TTBR0 4 KiB granule active!"), - Some(TCR_EL1::TG0::Value::KiB_16) => println!("[i] MMU: TTBR0 16 KiB granule active!"), - Some(TCR_EL1::TG0::Value::KiB_64) => println!("[i] MMU: TTBR0 64 KiB granule active!"), - _ => println!("[i] MMU: Invalid TTBR0 granule size specified!"), - } + Ok(PageTableEntry::PageDescriptor( + STAGE1_DESCRIPTOR::VALID::True + + STAGE1_DESCRIPTOR::AF::True + + into_mmu_attributes(attribute_fields) + + STAGE1_DESCRIPTOR::TYPE::Table + + STAGE1_DESCRIPTOR::NEXT_LVL_TABLE_ADDR_4KiB.val(shifted as u64), + )) + } +} - let t0sz = tcr.read(TCR_EL1::T0SZ); - println!("[i] MMU: T0sz = 64-{} = {} bits", t0sz, 64 - t0sz); +impl From for PageTableEntry { + fn from(_val: u64) -> PageTableEntry { + // xxx0 -> Invalid + // xx11 -> TableDescriptor on L0, L1 and L2 + // xx10 -> Block Entry L1 and L2 + // xx11 -> PageDescriptor L3 + PageTableEntry::Invalid + } +} - match tcr.read_as_enum(TCR_EL1::TG1) { - Some(TCR_EL1::TG1::Value::KiB_4) => println!("[i] MMU: TTBR1 4 KiB granule active!"), - Some(TCR_EL1::TG1::Value::KiB_16) => println!("[i] MMU: TTBR1 16 KiB granule active!"), - Some(TCR_EL1::TG1::Value::KiB_64) => println!("[i] MMU: TTBR1 64 KiB granule active!"), - _ => println!("[i] MMU: Invalid TTBR1 granule size specified!"), +impl From for u64 { + fn from(val: PageTableEntry) -> u64 { + match val { + PageTableEntry::Invalid => 0, + PageTableEntry::TableDescriptor(x) + | PageTableEntry::Lvl2BlockDescriptor(x) + | PageTableEntry::PageDescriptor(x) => x.value, } + } +} + +// to get L0 we must allocate a few frames from boot region allocator. +// So, first we init the dtb, parse mem-regions from there, then init boot_info page and start mmu, +// this part will be inited in mmu::init(): + +// @todo do NOT keep these statically, always allocate from available bump memory +// static mut LVL2_TABLE: Table = Table:: { +// entries: [0; NUM_ENTRIES_4KIB as usize], +// level: PhantomData, +// }; + +// @todo do NOT keep these statically, always allocate from available bump memory +// static mut LVL3_TABLE: Table = Table:: { +// entries: [0; NUM_ENTRIES_4KIB as usize], +// level: PhantomData, +// }; + +trait BaseAddr { + fn base_addr_u64(&self) -> u64; + fn base_addr_usize(&self) -> usize; +} + +impl BaseAddr for [u64; 512] { + fn base_addr_u64(&self) -> u64 { + self as *const u64 as u64 + } - let t1sz = tcr.read(TCR_EL1::T1SZ); - println!("[i] MMU: T1sz = 64-{} = {} bits", t1sz, 64 - t1sz); + fn base_addr_usize(&self) -> usize { + self as *const u64 as usize } } + +/// Set up identity mapped page tables for the first 1 gigabyte of address space. +/// default: 880 MB ARM ram, 128MB VC +/// +/// # Safety +/// +/// Completely unsafe, we're in the hardware land! Incorrectly initialised tables will just +/// restart the CPU. +pub unsafe fn init() -> Result<(), &'static str> { + // Prepare the memory attribute indirection register. + mair::set_up(); + + // Point the first 2 MiB of virtual addresses to the follow-up LVL3 + // page-table. + LVL2_TABLE.entries[0] = + PageTableEntry::new_table_descriptor(LVL3_TABLE.entries.base_addr_usize())?.into(); + + // Fill the rest of the LVL2 (2 MiB) entries as block descriptors. + // + // Notice the skip(1) which makes the iteration start at the second 2 MiB + // block (0x20_0000). + for (block_descriptor_nr, entry) in LVL2_TABLE.entries.iter_mut().enumerate().skip(1) { + let virt_addr = block_descriptor_nr << Size2MiB::SHIFT; + + let (output_addr, attribute_fields) = match get_virt_addr_properties(virt_addr) { + Err(s) => return Err(s), + Ok((a, b)) => (a, b), + }; + + let block_desc = + match PageTableEntry::new_lvl2_block_descriptor(output_addr, attribute_fields) { + Err(s) => return Err(s), + Ok(desc) => desc, + }; + + *entry = block_desc.into(); + } + + // Finally, fill the single LVL3 table (4 KiB granule). + for (page_descriptor_nr, entry) in LVL3_TABLE.entries.iter_mut().enumerate() { + let virt_addr = page_descriptor_nr << Size4KiB::SHIFT; + + let (output_addr, attribute_fields) = match get_virt_addr_properties(virt_addr) { + Err(s) => return Err(s), + Ok((a, b)) => (a, b), + }; + + let page_desc = match PageTableEntry::new_page_descriptor(output_addr, attribute_fields) { + Err(s) => return Err(s), + Ok(desc) => desc, + }; + + *entry = page_desc.into(); + } + + // Point to the LVL2 table base address in TTBR0. + TTBR0_EL1.set_baddr(LVL2_TABLE.entries.base_addr_u64()); // User (lo-)space addresses + + // TTBR1_EL1.set_baddr(LVL2_TABLE.entries.base_addr_u64()); // Kernel (hi-)space addresses + + // Configure various settings of stage 1 of the EL1 translation regime. + let ips = ID_AA64MMFR0_EL1.read(ID_AA64MMFR0_EL1::PARange); + TCR_EL1.write( + TCR_EL1::TBI0::Ignored // @todo TBI1 also set to Ignored?? + + TCR_EL1::IPS.val(ips) // Intermediate Physical Address Size + // ttbr0 user memory addresses + + TCR_EL1::TG0::KiB_4 // 4 KiB granule + + TCR_EL1::SH0::Inner + + TCR_EL1::ORGN0::WriteBack_ReadAlloc_WriteAlloc_Cacheable + + TCR_EL1::IRGN0::WriteBack_ReadAlloc_WriteAlloc_Cacheable + + TCR_EL1::EPD0::EnableTTBR0Walks + + TCR_EL1::T0SZ.val(34) // ARMv8ARM Table D5-11 minimum TxSZ for starting table level 2 + // ttbr1 kernel memory addresses + + TCR_EL1::TG1::KiB_4 // 4 KiB granule + + TCR_EL1::SH1::Inner + + TCR_EL1::ORGN1::WriteBack_ReadAlloc_WriteAlloc_Cacheable + + TCR_EL1::IRGN1::WriteBack_ReadAlloc_WriteAlloc_Cacheable + + TCR_EL1::EPD1::EnableTTBR1Walks + + TCR_EL1::T1SZ.val(34), // ARMv8ARM Table D5-11 minimum TxSZ for starting table level 2 + ); + + // Switch the MMU on. + // + // First, force all previous changes to be seen before the MMU is enabled. + barrier::isb(barrier::SY); + + // use cortex_a::regs::RegisterReadWrite; + // Enable the MMU and turn on data and instruction caching. + SCTLR_EL1.modify(SCTLR_EL1::M::Enable + SCTLR_EL1::C::Cacheable + SCTLR_EL1::I::Cacheable); + + // Force MMU init to complete before next instruction + /* + * Invalidate the local I-cache so that any instructions fetched + * speculatively from the PoC are discarded, since they may have + * been dynamically patched at the PoU. + */ + barrier::isb(barrier::SY); + + Ok(()) +} + +/// A function that maps the generic memory range attributes to HW-specific +/// attributes of the MMU. +fn into_mmu_attributes( + attribute_fields: AttributeFields, +) -> FieldValue { + use super::{AccessPermissions, MemAttributes}; + + // Memory attributes + let mut desc = match attribute_fields.mem_attributes { + MemAttributes::CacheableDRAM => { + STAGE1_DESCRIPTOR::SH::InnerShareable + + STAGE1_DESCRIPTOR::AttrIndx.val(mair::attr::NORMAL) + } + MemAttributes::NonCacheableDRAM => { + STAGE1_DESCRIPTOR::SH::InnerShareable + + STAGE1_DESCRIPTOR::AttrIndx.val(mair::attr::NORMAL_NON_CACHEABLE) + } + MemAttributes::Device => { + STAGE1_DESCRIPTOR::SH::OuterShareable + + STAGE1_DESCRIPTOR::AttrIndx.val(mair::attr::DEVICE_NGNRE) + } + }; + + // Access Permissions + desc += match attribute_fields.acc_perms { + AccessPermissions::ReadOnly => STAGE1_DESCRIPTOR::AP::RO_EL1, + AccessPermissions::ReadWrite => STAGE1_DESCRIPTOR::AP::RW_EL1, + }; + + // Execute Never + desc += if attribute_fields.execute_never { + STAGE1_DESCRIPTOR::PXN::NeverExecute + } else { + STAGE1_DESCRIPTOR::PXN::Execute + }; + + desc +} diff --git a/libs/memory/src/arch/aarch64/mmu2.rs b/libs/memory/src/arch/aarch64/mmu2.rs new file mode 100644 index 000000000..7548cbf7d --- /dev/null +++ b/libs/memory/src/arch/aarch64/mmu2.rs @@ -0,0 +1,631 @@ +/* + * SPDX-License-Identifier: BlueOak-1.0.0 + * Copyright (c) Berkus Decker + */ + +//! MMU initialisation. +//! +//! Paging is mostly based on [previous version](https://os.phil-opp.com/page-tables/) of +//! Phil Opp's [paging guide](https://os.phil-opp.com/paging-implementation/) and +//! [ARMv8 ARM memory addressing](https://static.docs.arm.com/100940/0100/armv8_a_address%20translation_100940_0100_en.pdf). +//! It includes ideas from Sergio Benitez' cs140e OSdev course material on type-safe access. + +#![allow(dead_code)] + +use { + crate::memory::{PhysAddr, VirtAddr, PhysFrame, PageSize}, + core::{ + marker::PhantomData, + ops::{Index, IndexMut}, + ptr::Unique, + }, + snafu::Snafu, + tock_registers::{fields, register_bitfields, LocalRegisterCopy}, +}; + +#[derive(Debug, Snafu)] +enum MmuError {} + +/* +/// Set up identity mapped page tables for the first 1 gigabyte of address space. +/// default: 880 MB ARM ram, 128MB VC +/// +/// # Safety +/// +/// Completely unsafe, we're in the hardware land! Incorrectly initialised tables will just +/// restart the CPU. +pub fn init() -> Result<(), MmuError> { + // Prepare the "memory attribute indirection register". + mair::set_up(); + + // @todo Do not map entire RAM, map only loaded and allocated space. + // Also find and map framebuffer -- the user-mode framebuffer driver should be able to do that. + + // should receive in args an obtained memory map from DT + let memory_map = Regions { + start: 0x1000, + size: 0x10000, + }; + + // bump-allocate page tables for entire memory + // also allocate phys memory to kernel space! + // + // separate regions - regular memory, device mmaps, + // initial thread maps ALL the memory?? + // instead + // init thread may map only necessary mem + // boot time only map kernel physmem space, and currently loaded kernel data + // PROBABLY only kernel mapping TTBR1 is needed, the rest is not very useful? + // take over protected memory space though anyway. + + // Point the first 2 MiB of virtual addresses to the follow-up LVL3 + // page-table. + // LVL2_TABLE.entries[0] = + // PageTableEntry::new_table_descriptor(LVL3_TABLE.entries.base_addr_usize())?.into(); + + // Fill the rest of the LVL2 (2 MiB) entries as block descriptors. + // + // Notice the skip(1) which makes the iteration start at the second 2 MiB + // block (0x20_0000). + for (block_descriptor_nr, entry) in LVL2_TABLE.entries.iter_mut().enumerate().skip(1) { + let virt_addr = block_descriptor_nr << Size2MiB::SHIFT; + + let (output_addr, attribute_fields) = match get_virt_addr_properties(virt_addr) { + Err(s) => return Err(s), + Ok((a, b)) => (a, b), + }; + + let block_desc = + match PageTableEntry::new_lvl2_block_descriptor(output_addr, attribute_fields) { + Err(s) => return Err(s), + Ok(desc) => desc, + }; + + *entry = block_desc.into(); + } + + // Finally, fill the single LVL3 table (4 KiB granule). + for (page_descriptor_nr, entry) in LVL3_TABLE.entries.iter_mut().enumerate() { + let virt_addr = page_descriptor_nr << Size4KiB::SHIFT; + + let (output_addr, attribute_fields) = match get_virt_addr_properties(virt_addr) { + Err(s) => return Err(s), + Ok((a, b)) => (a, b), + }; + + let page_desc = match PageTableEntry::new_page_descriptor(output_addr, attribute_fields) { + Err(s) => return Err(s), + Ok(desc) => desc, + }; + + *entry = page_desc.into(); + } +}*/ + +/* + * With 4k page granule, a virtual address is split into 4 lookup parts + * spanning 9 bits each: + * + * _______________________________________________ + * | | | | | | | + * | signx | Lv0 | Lv1 | Lv2 | Lv3 | off | + * |_______|_______|_______|_______|_______|_______| + * 63-48 47-39 38-30 29-21 20-12 11-00 + * + * mask page size + * + * Lv0: FF8000000000 -- + * Lv1: 7FC0000000 + * off: 3FFFFFFF 1G + * Lv2: 3FE00000 + * off: 1FFFFF 2M + * Lv3: 1FF000 + * off: FFF 4K + * + * RPi3 supports 64K and 4K granules, also 40-bit physical addresses. + * It also can address only 1G physical memory, so these 40-bit phys addresses are a fake. + * RPi4 can address more (up to 8Gb physical memory, 33 bits phys address, SoC has a so-called + * Full 35-bit address mode). + * + * 48-bit virtual address space; different mappings in VBAR0 (EL0) and VBAR1 (EL1+). + */ + +register_bitfields! { + u64, + VA_INDEX [ + LEVEL0 OFFSET(39) NUMBITS(9) [], + LEVEL1 OFFSET(30) NUMBITS(9) [], + LEVEL2 OFFSET(21) NUMBITS(9) [], + LEVEL3 OFFSET(12) NUMBITS(9) [], + OFFSET OFFSET(0) NUMBITS(12) [], + ] +} + +register_bitfields! { + u64, + // AArch64 Reference Manual page 2150, D5-2445 + TABLE_DESCRIPTOR [ + // In table descriptors + + NSTable_EL3 OFFSET(63) NUMBITS(1) [], + + /// Access Permissions for subsequent tables + APTable OFFSET(61) NUMBITS(2) [ + RW_EL1 = 0b00, + RW_EL1_EL0 = 0b01, + RO_EL1 = 0b10, + RO_EL1_EL0 = 0b11 + ], + + // User execute-never for subsequent tables + UXNTable OFFSET(60) NUMBITS(1) [ + Execute = 0, + NeverExecute = 1 + ], + + /// Privileged execute-never for subsequent tables + PXNTable OFFSET(59) NUMBITS(1) [ + Execute = 0, + NeverExecute = 1 + ], + + // In block descriptors + + // OS-specific data + OSData OFFSET(55) NUMBITS(4) [], + + // User execute-never + UXN OFFSET(54) NUMBITS(1) [ + Execute = 0, + NeverExecute = 1 + ], + + /// Privileged execute-never + PXN OFFSET(53) NUMBITS(1) [ + Execute = 0, + NeverExecute = 1 + ], + + // @fixme ?? where is this described + CONTIGUOUS OFFSET(52) NUMBITS(1) [ + False = 0, + True = 1 + ], + + // @fixme ?? where is this described + DIRTY OFFSET(51) NUMBITS(1) [ + False = 0, + True = 1 + ], + + /// Various address fields, depending on use case + LVL2_OUTPUT_ADDR_4KiB OFFSET(21) NUMBITS(27) [], // [47:21] + NEXT_LVL_TABLE_ADDR_4KiB OFFSET(12) NUMBITS(36) [], // [47:12] + + // @fixme ?? where is this described + NON_GLOBAL OFFSET(11) NUMBITS(1) [ + False = 0, + True = 1 + ], + + /// Access flag + AF OFFSET(10) NUMBITS(1) [ + False = 0, + True = 1 + ], + + /// Share-ability field + SH OFFSET(8) NUMBITS(2) [ + OuterShareable = 0b10, + InnerShareable = 0b11 + ], + + /// Access Permissions + AP OFFSET(6) NUMBITS(2) [ + RW_EL1 = 0b00, + RW_EL1_EL0 = 0b01, + RO_EL1 = 0b10, + RO_EL1_EL0 = 0b11 + ], + + NS_EL3 OFFSET(5) NUMBITS(1) [], + + /// Memory attributes index into the MAIR_EL1 register + AttrIndx OFFSET(2) NUMBITS(3) [], + + TYPE OFFSET(1) NUMBITS(1) [ + Block = 0, + Table = 1 + ], + + VALID OFFSET(0) NUMBITS(1) [ + False = 0, + True = 1 + ] + ] +} + +// type VaIndex = tock_registers::fields::FieldValue; +type VaType = LocalRegisterCopy; +type EntryFlags = fields::FieldValue; +type EntryRegister = LocalRegisterCopy; + +// Possible mappings: +// * TTBR0 pointing to user page global directory +// * TTBR0 pointing to user page upper directory (only if mmu is set up differently) +// * TTBR1 pointing to kernel page global directory with full physmem access + +/*! + * Paging system uses a separate address space in top kernel region (TTBR1) to access + * entire physical memory contents. + * This mapping is not available to user space (user space uses TTBR0). + * + * Use the largest possible granule size to map physical memory since we want to use + * the least amount of memory for these mappings. + */ + +// TTBR0 Page Global Directory + +// Level 0 descriptors can only output the address of a Level 1 table. +// Level 3 descriptors cannot point to another table and can only output block addresses. +// The format of the table is therefore slightly different for Level 3. +// +// this means: +// - in level 0 page table can be only TableDescriptors +// - in level 1,2 page table can be TableDescriptors, Lvl2BlockDescriptors (PageDescriptors) +// - in level 3 page table can be only PageDescriptors + +// Level / Types | Table Descriptor | Lvl2BlockDescriptor (PageDescriptor) +// --------------+------------------+-------------------------------------- +// 0 | X | (with 4KiB granule) +// 1 | X | X (1GiB range) +// 2 | X | X (2MiB range) +// 3 | | X (4KiB range) -- called PageDescriptor +// encoding actually the same as in Table Descriptor + +// Translation granule affects the size of the block addressed. +// Lets use 4KiB granule on RPi3 for simplicity. + +// 1, set 4KiB granule size to use the PGD - we could use 16KiB granule instead? +// - need to measure waste level +// - but lets stick with 4KiB for now +// + +// If I have, for example, Table I can get from it N `Table` (via impl HierarchicalTable) +// From Table I can get either `Table` (via impl HierarchicalTable) or `BlockDescriptor` +// From Table I can get either `Table` (via impl HierarchicalTable) or `BlockDescriptor` +// From Table I can only get `PageDescriptor` (because no impl HierarchicalTable exists) + +/// GlobalDirectory [ UpperDirectory entries ] +/// UpperDirectory [ PageDirectory | GiantPage ] +/// PageDirectory [ PageTable | LargePage ] +/// PageTable [ PageFrames ] + +// do those as separate types, then in accessors allow only certain combinations +// e.g. +// struct UpperDirectoryEntry; // DirectoryEntry +// struct PageDirectoryEntry; // DirectoryEntry +// struct GiantPageFrame; // PageFrame +// struct PageTableEntry; // DirectoryEntry +// struct LargePageFrame; // PageFrame +// struct PageFrame; // PageFrame + +// enum PageTableEntry { Page(&mut PageDescriptor), Block(&mut BlockDescriptor), Etc(&mut u64), Invalid(&mut u64) } +// impl PageTabelEntry { fn new_from_entry_addr(&u64) } +// return enum PageTableEntry constructed from table bits in u64 + +enum L0Entries { + UpperDirectoryEntry(VirtAddr), +} +enum L1Entries { + PageDirectoryEntry(VirtAddr), + GiantPageFrame(PhysFrame), +} +enum L2Entries { + PageTableEntry(VirtAddr), + LargePageFrame(PhysFrame), +} +enum L3Entries { + PageFrame(PhysFrame), +} + +enum Frames { + GiantPageFrame, + LargePageFrame, + PageFrame, +} + +// ---- +// ---- +// ---- Table levels +// ---- +// ---- + +/// L0 table -- only pointers to L1 tables +pub enum L0PageGlobalDirectory {} +/// L1 tables -- pointers to L2 tables or giant 1GiB pages +pub enum L1PageUpperDirectory {} +/// L2 tables -- pointers to L3 tables or huge 2MiB pages +pub enum L2PageDirectory {} +/// L3 tables -- only pointers to 4/16KiB pages +pub enum L3PageTable {} + +/// Shared trait for specific table levels. +pub trait TableLevel {} + +/// Shared trait for hierarchical table levels. +/// +/// Specifies what is the next level of page table hierarchy. +pub trait HierarchicalLevel: TableLevel { + /// Level of the next translation table below this one. + type NextLevel: TableLevel; + + // fn translate() -> Directory; +} + +/// Specify allowed page size for each level. +pub trait HierarchicalPageLevel: TableLevel { + /// Size of the page that can be contained in this table level. + type PageLevel: PageSize; +} + +impl TableLevel for L0PageGlobalDirectory {} +impl TableLevel for L1PageUpperDirectory {} +impl TableLevel for L2PageDirectory {} +impl TableLevel for L3PageTable {} + +impl HierarchicalLevel for L0PageGlobalDirectory { + type NextLevel = L1PageUpperDirectory; +} +impl HierarchicalLevel for L1PageUpperDirectory { + type NextLevel = L2PageDirectory; +} +impl HierarchicalLevel for L2PageDirectory { + type NextLevel = L3PageTable; +} +// L3PageTables do not have next level, therefore they are not HierarchicalLevel + +// L0PageGlobalDirectory does not contain pages, so they are not HierarchicalPageLevel +impl HierarchicalPageLevel for L1PageUpperDirectory { + type PageLevel = Page; +} +impl HierarchicalPageLevel for L2PageDirectory { + type PageLevel = Page; +} +impl HierarchicalPageLevel for L3PageTable { + type PageLevel = Page; +} + +// ---- +// ---- +// ---- Directory +// ---- +// ---- + +// Maximum OA is 48 bits. +// +// Level 0 table descriptor has Output Address in [47:12] --> level 1 table +// Level 0 descriptor cannot be block descriptor. +// +// Level 1 table descriptor has Output Address in [47:12] --> level 2 table +// Level 1 block descriptor has Output Address in [47:30] +// +// Level 2 table descriptor has Output Address in [47:12] --> level 3 table +// Level 2 block descriptor has Output Address in [47:21] +// +// Level 3 block descriptor has Output Address in [47:12] +// Upper Attributes [63:51] +// Res0 [50:48] +// Lower Attributes [11:2] +// 11b [1:0] + +// Each table consists of 2**9 entries +const TABLE_BITS: usize = 9; +const INDEX_MASK: usize = (1 << TABLE_BITS) - 1; + +static_assertions::const_assert!(INDEX_MASK == 0x1ff); + +// @todo Table in mmu.rs +/// MMU address translation table. +/// Contains just u64 internally, provides enum interface on top +#[repr(C)] +#[repr(align(4096))] +struct Directory { + entries: [u64; 1 << TABLE_BITS], + level: PhantomData, +} + +impl Directory { + fn next(&self, address: VirtAddr) -> Option { + let va = VaType::new(address.as_u64()); + let index = va.read(VA_INDEX::LEVEL0); + match self.next_table_address(index as usize) { + Some(phys_addr) => { + Some(L0Entries::UpperDirectoryEntry(phys_addr.user_to_kernel())) + } + None => None, + } + } +} + +impl Directory { + fn next(&self, address: VirtAddr) -> Option { + let va = VaType::new(address.as_u64()); + let index = va.read(VA_INDEX::LEVEL1); + match self.next_table_address(index as usize) { + Some(phys_addr) => { + Some(L1Entries::PageDirectoryEntry(phys_addr.user_to_kernel())) + } + None => None, // @todo could be 1GiB frame + } + } +} + +impl Directory { + fn next(&self, address: VirtAddr) -> Option { + let va = VaType::new(address.as_u64()); + let index = va.read(VA_INDEX::LEVEL2); + match self.next_table_address(index as usize) { + Some(phys_addr) => { + Some(L2Entries::PageTableEntry(phys_addr.user_to_kernel())) + } + None => None, // @todo could be 2MiB frame + } + } +} + +impl Directory { + fn next(&self, address: VirtAddr) -> Option { + let va = VaType::new(address.as_u64()); + let _index = va.read(VA_INDEX::LEVEL3); + // @fixme wrong function + // match self.next_table_address(index as usize) { + // Some(phys_addr) => Some(L3Entries::PageFrame(phys_addr.user_to_kernel())), + // None => None, // Nothing there + // } + None + } +} + +// Implementation code shared for all levels of page tables +impl Directory +where + Level: TableLevel, +{ + /// Construct a zeroed table at given physical location. + // unsafe fn at(location: PhysAddr) -> &Self {} + + /// Construct and return zeroed table. + fn zeroed() -> Self { + Self { + entries: [0; 1 << TABLE_BITS], + level: PhantomData, + } + } + + /// Zero out entire table. + pub fn zero(&mut self) { + for entry in self.entries.iter_mut() { + *entry = 0; + } + } +} + +impl Index for Directory +where + Level: TableLevel, +{ + type Output = u64; + + fn index(&self, index: usize) -> &Self::Output { + &self.entries[index] + } +} + +impl IndexMut for Directory +where + Level: TableLevel, +{ + fn index_mut(&mut self, index: usize) -> &mut Self::Output { + &mut self.entries[index] + } +} + +impl Directory +where + Level: HierarchicalLevel, +{ + fn next_table_address(&self, index: usize) -> Option { + let entry_flags = EntryRegister::new(self[index]); + // If table entry has 0b11 mask set, it is a valid table entry. + // Address of the following table may be extracted from bits 47:12 + if entry_flags.matches_all(TABLE_DESCRIPTOR::VALID::True + TABLE_DESCRIPTOR::TYPE::Table) { + Some(PhysAddr::new( + entry_flags.read(TABLE_DESCRIPTOR::NEXT_LVL_TABLE_ADDR_4KiB) << Size4KiB::SHIFT, + )) + } else { + None + } + } + + pub fn next_table(&self, index: usize) -> Option<&Table> { + self.next_table_address(index) + .map(|address| unsafe { &*(address.user_to_kernel().as_ptr()) }) + } + + pub fn next_table_mut(&mut self, index: usize) -> Option<&mut Table> { + self.next_table_address(index) + .map(|address| unsafe { &mut *(address.user_to_kernel().as_mut_ptr()) }) + } + + pub fn translate_levels(&self, _address: VirtAddr) -> Option { + None + } +} + +// ---- +// ---- +// ---- VirtSpace +// ---- +// ---- + +/// Errors from mapping layer +#[derive(Debug, Snafu)] +pub enum TranslationError { + /// No page found. @todo + NoPage, +} + +/// Virtual address space. @todo +pub struct VirtSpace { + l0: Unique>, +} + +// translation steps: +// l0: upper page directory or Err() +// l1: lower page directory or 1Gb aperture or Err() +// l2: page table or 2MiB aperture or Err() +// l3: 4KiB aperture or Err() + +impl VirtSpace { + // Translate translates address all the way down to physical address or error. + // On each level there's next_table() fn that resolves to the next level table if possible. + pub fn translate(&self, virtual_address: VirtAddr) -> Result { + // let offset = virtual_address % Self::PageLevel::SIZE as usize; // use the size of the last page? + self.translate_page(Self::PageLevel::containing_address(virtual_address))? + .map(|frame, offset| frame.start_address() + offset) + } +} + +// pageglobaldirectory.translate() { +// get page index <- generic over page level (xx << (10 + (3 - level) * 9)) +// return page[index]?.translate(rest); +// } + +#[cfg(test)] +mod tests { + use super::*; + + #[test_case] + fn table_construction() { + let mut level0_table = Directory::::zeroed(); + let level1_table = Directory::::zeroed(); + let level2_table = Directory::::zeroed(); + let level3_table = Directory::::zeroed(); + + assert!(level0_table.next_table_address(0).is_none()); + + // Make entry map to a level1 table + level0_table[0] = EntryFlags::from( + TABLE_DESCRIPTOR::VALID::True + + TABLE_DESCRIPTOR::TYPE::Table + + TABLE_DESCRIPTOR::NEXT_LVL_TABLE_ADDR_4KiB.val(0x424242), + ) + .into(); + + assert!(level0_table.next_table_address(0).is_some()); + + let addr = level0_table.next_table_address(0).unwrap(); + assert_eq!(addr.as_u64(), (0x424242u64 << 12)); + } +} diff --git a/libs/memory/src/arch/aarch64/mmu_experimental.rs b/libs/memory/src/arch/aarch64/mmu_experimental.rs new file mode 100644 index 000000000..ba56414d1 --- /dev/null +++ b/libs/memory/src/arch/aarch64/mmu_experimental.rs @@ -0,0 +1,279 @@ +// Check largest VA supported, calculate physical_memory_offset +// +const PHYSICAL_MEMORY_OFFSET: u64 = 0xffff_8000_0000_0000; // Last 1GiB of VA space + +// AArch64: +// Table D4-8-2021: check supported granule sizes, select alloc policy based on results. +// TTBR_ELx is the pdbr for specific page tables + +// Page 2068 actual page descriptor formats + +/// Errors from mapping layer +#[derive(Debug, Snafu)] +pub enum TranslationError { + NoPage, +} + +// Pointer to currently active page table +// Could be either user space (TTBR0) or kernel space (TTBR1) -- ?? +pub struct ActivePageTable { + l0: Unique>, +} + +impl ActivePageTable { + pub unsafe fn new() -> ActivePageTable { + ActivePageTable { + l0: Unique::new_unchecked(0 as *mut _), + } + } + + fn l0(&self) -> &Table { + unsafe { self.l0.as_ref() } + } + + fn l0_mut(&mut self) -> &mut Table { + unsafe { self.l0.as_mut() } + } + + // pub fn translate(&self, virtual_address: VirtAddr) -> Result { + // let offset = virtual_address % Size4KiB::SIZE as usize; // @todo use the size of the last page of course + // self.translate_page(Page::containing_address(virtual_address)) + // .map(|frame| frame.start_address() + offset) + // } + + fn translate_page(&self, page: Page) -> Result { + // @todo translate only one level of hierarchy per impl function... + let l1 = self.l0().next_table(u64::from(page.l0_index()) as usize); + /* + let huge_page = || { + l1.and_then(|l1| { + let l1_entry = &l1[page.l1_index() as usize]; + // 1GiB page? + if let Some(start_frame) = l1_entry.pointed_frame() { + if l1_entry.flags().read(STAGE1_DESCRIPTOR::TYPE) + != STAGE1_DESCRIPTOR::TYPE::Table.value + { + // address must be 1GiB aligned + //start_frame.is_aligned() + assert!(start_frame.number % (NUM_ENTRIES_4KIB * NUM_ENTRIES_4KIB) == 0); + return Ok(PhysFrame::from_start_address( + start_frame.number + + page.l2_index() * NUM_ENTRIES_4KIB + + page.l3_index(), + )); + } + } + if let Some(l2) = l1.next_table(page.l1_index()) { + let l2_entry = &l2[page.l2_index()]; + // 2MiB page? + if let Some(start_frame) = l2_entry.pointed_frame() { + if l2_entry.flags().read(STAGE1_DESCRIPTOR::TYPE) + != STAGE1_DESCRIPTOR::TYPE::Table + { + // address must be 2MiB aligned + assert!(start_frame.number % NUM_ENTRIES_4KIB == 0); + return Ok(PhysFrame::from_start_address( + start_frame.number + page.l3_index(), + )); + } + } + } + Err(TranslationError::NoPage) + }) + }; + */ + let v = l1 + .and_then(|l1| l1.next_table(u64::from(page.l1_index()) as usize)) + .and_then(|l2| l2.next_table(u64::from(page.l2_index()) as usize)) + .and_then(|l3| Some(l3[u64::from(page.l3_index()) as usize])); //.pointed_frame()) + // .ok_or(TranslationError::NoPage) + // .or_else(huge_page) + Ok(v.unwrap().into()) + } + + pub fn map_to(&mut self, page: Page, frame: PhysFrame, flags: EntryFlags, allocator: &mut A) + where + A: FrameAllocator, + { + let l0 = self.l0_mut(); + let l1 = l0.next_table_create(u64::from(page.l0_index()) as usize, allocator); + let l2 = l1.next_table_create(u64::from(page.l1_index()) as usize, allocator); + let l3 = l2.next_table_create(u64::from(page.l2_index()) as usize, allocator); + + assert_eq!( + l3[u64::from(page.l3_index()) as usize], + 0 /*.is_unused()*/ + ); + l3[u64::from(page.l3_index()) as usize] = PageTableEntry::PageDescriptor( + STAGE1_DESCRIPTOR::NEXT_LVL_TABLE_ADDR_4KiB.val(u64::from(frame)) + + flags // @todo properly extract flags + + STAGE1_DESCRIPTOR::VALID::True, + ) + .into(); + } + + pub fn map(&mut self, page: Page, flags: EntryFlags, allocator: &mut A) + where + A: FrameAllocator, + { + // @todo fail mapping if table is not allocated, causing client to allocate and restart + // @todo problems described in preso - chicken&egg problem of allocating first allocations + let frame = allocator.allocate_frame().expect("out of memory"); + self.map_to(page, frame, flags, allocator) + } + + pub fn identity_map(&mut self, frame: PhysFrame, flags: EntryFlags, allocator: &mut A) + where + A: FrameAllocator, + { + let page = Page::containing_address(VirtAddr::new(frame.start_address().as_u64())); + self.map_to(page, frame, flags, allocator) + } + + fn unmap(&mut self, page: Page, _allocator: &mut A) + where + A: FrameAllocator, + { + // use aarch64::instructions::tlb; + // use x86_64::VirtAddr; + + assert!(self.translate(page.start_address()).is_ok()); + + let l3 = self + .l0_mut() + .next_table_mut(u64::from(page.l0_index()) as usize) + .and_then(|l1| l1.next_table_mut(u64::from(page.l1_index()) as usize)) + .and_then(|l2| l2.next_table_mut(u64::from(page.l2_index()) as usize)) + .expect("mapping code does not support huge pages"); + let _frame = l3[u64::from(page.l3_index()) as usize]; + // .pointed_frame() + // .unwrap(); + l3[u64::from(page.l3_index()) as usize] = 0; /*.set_unused(); */ + // tlb::flush(VirtAddr(page.start_address())); + // TODO free p(1,2,3) table if empty + //allocator.deallocate_frame(frame); + // @todo do NOT deallocate frames either, but need to signal client that it's unused + } +} + +// Abstractions for page table entries. + +/// The error returned by the `PageTableEntry::frame` method. +#[derive(Snafu, Debug, Clone, Copy, PartialEq)] +pub enum FrameError { + /// The entry does not have the `PRESENT` flag set, so it isn't currently mapped to a frame. + FrameNotPresent, + /// The entry has the `HUGE_PAGE` flag set. The `frame` method has a standard 4KiB frame + /// as return type, so a huge frame can't be returned. @todo + HugeFrame, +} + +/// A 64-bit page table entry. +// pub struct PageTableEntry { +// entry: u64, +// } + +const ADDR_MASK: u64 = 0x0000_ffff_ffff_f000; +/* +impl PageTableEntry { + /// Creates an unused page table entry. + pub fn new() -> Self { + PageTableEntry::Invalid + } + + /// Returns whether this entry is zero. + pub fn is_unused(&self) -> bool { + self.entry == 0 + } + + /// Sets this entry to zero. + pub fn set_unused(&mut self) { + self.entry = 0; + } + + /// Returns the flags of this entry. + pub fn flags(&self) -> EntryFlags { + EntryFlags::new(self.entry) + } + + /// Returns the physical address mapped by this entry, might be zero. + pub fn addr(&self) -> PhysAddr { + PhysAddr::new(self.entry & ADDR_MASK) + } + + /// Returns the physical frame mapped by this entry. + /// + /// Returns the following errors: + /// + /// - `FrameError::FrameNotPresent` if the entry doesn't have the `PRESENT` flag set. + /// - `FrameError::HugeFrame` if the entry has the `HUGE_PAGE` flag set (for huge pages the + /// `addr` function must be used) + pub fn frame(&self) -> Result { + if !self.flags().read(STAGE1_DESCRIPTOR::VALID) { + Err(FrameError::FrameNotPresent) + // } else if self.flags().contains(EntryFlags::HUGE_PAGE) { + // Err(FrameError::HugeFrame) + } else { + Ok(PhysFrame::containing_address(self.addr())) + } + } + + /// Map the entry to the specified physical address with the specified flags. + pub fn set_addr(&mut self, addr: PhysAddr, flags: EntryFlags) { + assert!(addr.is_aligned(Size4KiB::SIZE)); + self.entry = addr.as_u64() | flags.bits(); + } + + /// Map the entry to the specified physical frame with the specified flags. + pub fn set_frame(&mut self, frame: PhysFrame, flags: EntryFlags) { + // assert!(!flags.contains(EntryFlags::HUGE_PAGE)); + self.set_addr(frame.start_address(), flags) + } + + /// Sets the flags of this entry. + pub fn set_flags(&mut self, flags: EntryFlags) { + // Todo: extract ADDR from self and replace all flags completely (?) + self.entry = self.addr().as_u64() | flags.bits(); + } +} + +impl fmt::Debug for PageTableEntry { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + let mut f = f.debug_struct("PageTableEntry"); + f.field("addr", &self.addr()); + f.field("flags", &self.flags()); + f.finish() + } +}*/ + +impl Table +where + Level: HierarchicalLevel, +{ + pub fn next_table_create( + &mut self, + index: usize, + allocator: &mut Alloc, + ) -> &mut Table + where + Alloc: FrameAllocator, + { + if self.next_table(index).is_none() { + assert!( + EntryRegister::new(self.entries[index]).read(STAGE1_DESCRIPTOR::TYPE) + == STAGE1_DESCRIPTOR::TYPE::Table.value, + "mapping code does not support huge pages" + ); + let frame = allocator.allocate_frame().expect("no frames available"); + self.entries[index] = PageTableEntry::TableDescriptor( + STAGE1_DESCRIPTOR::NEXT_LVL_TABLE_ADDR_4KiB.val(u64::from(frame)) + + STAGE1_DESCRIPTOR::VALID::True, + ) + .into(); + // self.entries[index] + // .set_frame(frame, STAGE1_DESCRIPTOR::VALID::True /*| WRITABLE*/); + self.next_table_mut(index).unwrap().zero(); + } + self.next_table_mut(index).unwrap() + } +} diff --git a/libs/memory/src/arch/aarch64/mod.rs b/libs/memory/src/arch/aarch64/mod.rs index 8d551f9d9..b00861957 100644 --- a/libs/memory/src/arch/aarch64/mod.rs +++ b/libs/memory/src/arch/aarch64/mod.rs @@ -6,10 +6,30 @@ //! Memory management functions for aarch64. mod addr; +pub mod features; // @todo make only pub re-export? pub mod mmu; +mod page_size; +mod phys_frame; +mod virt_page; -// pub use addr::{PhysAddr, VirtAddr}; +// mod area_frame_allocator; +// pub use self::area_frame_allocator::AreaFrameAllocator; +// mod boot_allocator; // Hands out physical memory obtained from devtree +// use self::paging::PAGE_SIZE; +// pub use crate::memory::{PhysAddr, VirtAddr}; +pub use {page_size::PageSize, phys_frame::PhysFrame}; + +/// @todo ?? +pub trait FrameAllocator { + /// Allocate a physical memory frame. + fn allocate_frame(&mut self) -> Option; // @todo Result<> + /// Deallocate a physical frame. + fn deallocate_frame(&mut self, frame: PhysFrame); +} + +// Identity-map things for now. +// // aarch64 granules and page sizes howto: // https://stackoverflow.com/questions/34269185/simultaneous-existence-of-different-sized-pages-on-aarch64 diff --git a/libs/memory/src/arch/aarch64/page_size.rs b/libs/memory/src/arch/aarch64/page_size.rs new file mode 100644 index 000000000..e62b455f9 --- /dev/null +++ b/libs/memory/src/arch/aarch64/page_size.rs @@ -0,0 +1,68 @@ +/// Trait for abstracting over the possible page sizes, 4KiB, 16KiB, 2MiB, 1GiB. +pub trait PageSize: Copy + PartialEq + Eq + PartialOrd + Ord { + /// The page size in bytes. + const SIZE: usize; + + /// A string representation of the page size for debug output. + const SIZE_AS_DEBUG_STR: &'static str; + + /// The page shift in bits. + const SHIFT: usize; + + /// The page mask in bits. + const MASK: u64; +} + +/// This trait is implemented for 4KiB, 16KiB, and 2MiB pages, but not for 1GiB pages. +pub trait NotGiantPageSize: PageSize {} // @todo doesn't have to be pub?? + +/// A standard 4KiB page. +#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)] +pub enum Size4KiB {} + +impl PageSize for Size4KiB { + const SIZE: usize = 4 * 1024; + const SIZE_AS_DEBUG_STR: &'static str = "4KiB"; + const SHIFT: usize = 12; + const MASK: u64 = 0xfff; +} + +impl NotGiantPageSize for Size4KiB {} + +/// A standard 16KiB page. +/// Currently unused. +#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)] +pub enum Size16KiB {} + +impl PageSize for Size16KiB { + const SIZE: usize = 16 * 1024; + const SIZE_AS_DEBUG_STR: &'static str = "16KiB"; + const SHIFT: usize = 14; + const MASK: u64 = 0x3fff; +} + +impl NotGiantPageSize for Size16KiB {} + +/// A “huge” 2MiB page. +#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)] +pub enum Size2MiB {} + +impl PageSize for Size2MiB { + const SIZE: usize = 2 * 1024 * 1024; + const SIZE_AS_DEBUG_STR: &'static str = "2MiB"; + const SHIFT: usize = 21; + const MASK: u64 = 0x1f_ffff; +} + +impl NotGiantPageSize for Size2MiB {} + +/// A “giant” 1GiB page. +#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)] +pub enum Size1GiB {} + +impl PageSize for Size1GiB { + const SIZE: usize = 1024 * 1024 * 1024; + const SIZE_AS_DEBUG_STR: &'static str = "1GiB"; + const SHIFT: usize = 30; + const MASK: u64 = 0x3fff_ffff; +} diff --git a/libs/memory/src/arch/aarch64/phys_frame.rs b/libs/memory/src/arch/aarch64/phys_frame.rs new file mode 100644 index 000000000..8737448f8 --- /dev/null +++ b/libs/memory/src/arch/aarch64/phys_frame.rs @@ -0,0 +1,202 @@ +// Verbatim from https://github.com/rust-osdev/x86_64/blob/aa9ae54657beb87c2a491f2ab2140b2332afa6ba/src/structures/paging/frame.rs +// Abstractions for default-sized and huge physical memory frames. + +use { + crate::memory::{ + PhysAddr, + page_size::{PageSize, Size4KiB}, + }, + core::{ + fmt, + marker::PhantomData, + ops::{Add, AddAssign, Sub, SubAssign}, + }, +}; + +/// A physical memory frame. +/// Frame is an addressable unit of the physical address space. +#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +#[repr(C)] +pub struct PhysFrame { + start_address: PhysAddr, + size: PhantomData, +} + +impl From for PhysFrame { + fn from(address: u64) -> PhysFrame { + PhysFrame::containing_address(PhysAddr::new(address)) + } +} + +impl From> for u64 { + fn from(frame: PhysFrame) -> u64 { + frame.start_address.as_u64() + } +} + +impl PhysFrame { + /// Returns the frame that starts at the given virtual address. + /// + /// Returns an error if the address is not correctly aligned (i.e. is not a valid frame start). + pub fn from_start_address(address: PhysAddr) -> Result { + if !address.is_aligned(S::SIZE) { + return Err(()); + } + Ok(PhysFrame::containing_address(address)) + } + + /// Returns the frame that contains the given physical address. + pub fn containing_address(address: PhysAddr) -> Self { + PhysFrame { + start_address: address.aligned_down(S::SIZE), + size: PhantomData, + } + } + + /// Returns the start address of the frame. + pub fn start_address(&self) -> PhysAddr { + self.start_address + } + + /// Returns the size the frame (4KB, 2MB or 1GB). + pub fn size(&self) -> usize { + S::SIZE + } + + /// Returns a range of frames, exclusive `end`. + pub fn range(start: PhysFrame, end: PhysFrame) -> PhysFrameRange { + PhysFrameRange { start, end } + } + + /// Returns a range of frames, inclusive `end`. + pub fn range_inclusive(start: PhysFrame, end: PhysFrame) -> PhysFrameRangeInclusive { + PhysFrameRangeInclusive { start, end } + } +} + +impl fmt::Debug for PhysFrame { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + f.write_fmt(format_args!( + "PhysFrame[{}]({:#x})", + S::SIZE_AS_DEBUG_STR, + self.start_address().as_u64() + )) + } +} + +impl Add for PhysFrame { + type Output = Self; + /// Adds `rhs` same-sized frames to the current address. + fn add(self, rhs: u64) -> Self::Output { + PhysFrame::containing_address(self.start_address() + rhs * S::SIZE as u64) + } +} + +impl AddAssign for PhysFrame { + fn add_assign(&mut self, rhs: u64) { + *self = self.clone() + rhs; + } +} + +impl Sub for PhysFrame { + type Output = Self; + /// Subtracts `rhs` same-sized frames from the current address. + // @todo should I sub pages or just bytes here? + fn sub(self, rhs: u64) -> Self::Output { + PhysFrame::containing_address(self.start_address() - rhs * S::SIZE as u64) + } +} + +impl SubAssign for PhysFrame { + fn sub_assign(&mut self, rhs: u64) { + *self = self.clone() - rhs; + } +} + +impl Sub> for PhysFrame { + type Output = usize; + /// Return number of frames between start and end addresses. + fn sub(self, rhs: PhysFrame) -> Self::Output { + (self.start_address - rhs.start_address) as usize / S::SIZE + } +} + +/// A range of physical memory frames, exclusive the upper bound. +#[derive(Clone, Copy, PartialEq, Eq)] +#[repr(C)] +pub struct PhysFrameRange { + /// The start of the range, inclusive. + pub start: PhysFrame, + /// The end of the range, exclusive. + pub end: PhysFrame, +} + +impl PhysFrameRange { + /// Returns whether the range contains no frames. + pub fn is_empty(&self) -> bool { + !(self.start < self.end) + } +} + +impl Iterator for PhysFrameRange { + type Item = PhysFrame; + + fn next(&mut self) -> Option { + if !self.is_empty() { + let frame = self.start.clone(); + self.start += 1; + Some(frame) + } else { + None + } + } +} + +impl fmt::Debug for PhysFrameRange { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + f.debug_struct("PhysFrameRange") + .field("start", &self.start) + .field("end", &self.end) + .finish() + } +} + +/// An range of physical memory frames, inclusive the upper bound. +#[derive(Clone, Copy, PartialEq, Eq)] +#[repr(C)] +pub struct PhysFrameRangeInclusive { + /// The start of the range, inclusive. + pub start: PhysFrame, + /// The start of the range, exclusive. + pub end: PhysFrame, +} + +impl PhysFrameRangeInclusive { + /// Returns whether the range contains no frames. + pub fn is_empty(&self) -> bool { + !(self.start <= self.end) + } +} + +impl Iterator for PhysFrameRangeInclusive { + type Item = PhysFrame; + + fn next(&mut self) -> Option { + if !self.is_empty() { + let frame = self.start.clone(); + self.start += 1; + Some(frame) + } else { + None + } + } +} + +impl fmt::Debug for PhysFrameRangeInclusive { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + f.debug_struct("PhysFrameRangeInclusive") + .field("start", &self.start) + .field("end", &self.end) + .finish() + } +} diff --git a/libs/memory/src/arch/aarch64/virt_page.rs b/libs/memory/src/arch/aarch64/virt_page.rs new file mode 100644 index 000000000..92e7ffff7 --- /dev/null +++ b/libs/memory/src/arch/aarch64/virt_page.rs @@ -0,0 +1,336 @@ +// Verbatim from https://github.com/rust-osdev/x86_64/blob/aa9ae54657beb87c2a491f2ab2140b2332afa6ba/src/structures/paging/page.rs +// Abstractions for default-sized and huge virtual memory pages. + +// @fixme x86_64 page level numbering: P4 -> P3 -> P2 -> P1 +// @fixme armv8a page level numbering: L0 -> L1 -> L2 -> L3 + +#![allow(dead_code)] + +use { + crate::memory::{ + VirtAddr, + page_size::{NotGiantPageSize, PageSize, Size1GiB, Size2MiB, Size4KiB}, + }, + core::{ + fmt, + marker::PhantomData, + ops::{Add, AddAssign, Sub, SubAssign}, + }, + ux::u9, +}; + +/// A virtual memory page. +#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +pub struct Page { + start_address: VirtAddr, + size: PhantomData, +} + +pub enum Error { + NotAligned, +} + +impl Page { + /// The page size in bytes. + pub const SIZE: usize = S::SIZE; + + /// Returns the page that starts at the given virtual address. + /// + /// Returns an error if the address is not correctly aligned (i.e. is not a valid page start). + pub fn from_start_address(address: VirtAddr) -> Result { + if !address.is_aligned(S::SIZE) { + Err(Error::NotAligned) + } else { + Ok(Page::containing_address(address)) + } + } + + /// Returns the page that contains the given virtual address. + pub fn containing_address(address: VirtAddr) -> Self { + Page { + start_address: address.aligned_down(S::SIZE), + size: PhantomData, + } + } + + /// Returns the start address of the page. + pub fn start_address(&self) -> VirtAddr { + self.start_address + } + + /// Returns the size the page (4KB, 2MB or 1GB). + pub const fn size(&self) -> usize { + S::SIZE + } + + /// Returns the level 0 page table index of this page. + pub fn l0_index(&self) -> u9 { + self.start_address().l0_index() + } + + /// Returns the level 1 page table index of this page. + pub fn l1_index(&self) -> u9 { + self.start_address().l1_index() + } + + /// Returns a range of pages, exclusive `end`. + pub fn range(start: Self, end: Self) -> PageRange { + PageRange { start, end } + } + + /// Returns a range of pages, inclusive `end`. + pub fn range_inclusive(start: Self, end: Self) -> PageRangeInclusive { + PageRangeInclusive { start, end } + } +} + +impl Page { + /// Returns the level 2 page table index of this page. + pub fn l2_index(&self) -> u9 { + self.start_address().l2_index() + } +} + +impl Page { + /// Returns the 1GiB memory page with the specified page table indices. + pub fn from_page_table_indices_1gib(l0_index: u9, l1_index: u9) -> Self { + use bit_field::BitField; + + let mut addr = 0; + addr.set_bits(39..48, u64::from(l0_index)); + addr.set_bits(30..39, u64::from(l1_index)); + Page::containing_address(VirtAddr::new(addr)) + } +} + +impl Page { + /// Returns the 2MiB memory page with the specified page table indices. + pub fn from_page_table_indices_2mib(l0_index: u9, l1_index: u9, l2_index: u9) -> Self { + use bit_field::BitField; + + let mut addr = 0; + addr.set_bits(39..48, u64::from(l0_index)); + addr.set_bits(30..39, u64::from(l1_index)); + addr.set_bits(21..30, u64::from(l2_index)); + Page::containing_address(VirtAddr::new(addr)) + } +} + +impl Page { + /// Returns the 4KiB memory page with the specified page table indices. + pub fn from_page_table_indices(l0_index: u9, l1_index: u9, l2_index: u9, l3_index: u9) -> Self { + use bit_field::BitField; + + let mut addr = 0; + addr.set_bits(39..48, u64::from(l0_index)); + addr.set_bits(30..39, u64::from(l1_index)); + addr.set_bits(21..30, u64::from(l2_index)); + addr.set_bits(12..21, u64::from(l3_index)); + Page::containing_address(VirtAddr::new(addr)) + } + + /// Returns the level 3 page table index of this page. + pub fn l3_index(&self) -> u9 { + self.start_address().l3_index() + } +} + +impl fmt::Debug for Page { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + f.write_fmt(format_args!( + "Page<{}>({:#x})", + S::SIZE_AS_DEBUG_STR, + self.start_address().as_u64() + )) + } +} + +impl Add for Page { + type Output = Self; + /// Add a number of bytes to the current page and find the next page with the same size + /// containing the resulting address. + fn add(self, rhs: T) -> Self::Output { + Page::containing_address(self.start_address() + rhs) + } +} + +impl AddAssign for Page { + fn add_assign(&mut self, rhs: T) { + *self = *self + rhs; + } +} + +impl Sub for Page { + type Output = Self; + /// Subtract a number of bytes from the current page start address and find + /// the next page with the same size containing the resulting address. + fn sub(self, rhs: T) -> Self::Output { + Page::containing_address(self.start_address() - rhs) + } +} + +impl SubAssign for Page { + fn sub_assign(&mut self, rhs: T) { + *self = *self - rhs; + } +} + +impl Sub for Page { + type Output = usize; // @todo must be isize here? + /// Return number of bytes between two pages' starting addresses. + fn sub(self, rhs: Self) -> Self::Output { + (self.start_address - rhs.start_address) as usize + } +} + +/// A range of pages with exclusive upper bound. +#[derive(Clone, Copy, PartialEq, Eq)] +pub struct PageRange { + /// The start of the range, inclusive. + pub start: Page, + /// The end of the range, exclusive. + pub end: Page, +} + +impl PageRange { + /// Returns whether this range contains no pages. + pub fn is_empty(&self) -> bool { + self.start >= self.end + } + + pub fn num_pages(&self) -> usize { + (self.end - self.start) / S::SIZE + } +} + +impl Iterator for PageRange { + type Item = Page; + + fn next(&mut self) -> Option { + if !self.is_empty() { + let page = self.start; + self.start += S::SIZE; // @todo Destructive iterator + Some(page) + } else { + None + } + } +} + +impl PageRange { + /// Converts the range of 4KiB pages to a range of 4KiB pages (identity operation). + #[inline(always)] + pub fn as_4kib_page_range(&self) -> PageRange { + *self + } +} + +impl PageRange { + /// Converts the range of 2MiB pages to a range of 4KiB pages. + // @todo what about range of 1GiB pages? + pub fn as_4kib_page_range(&self) -> PageRange { + PageRange { + start: Page::containing_address(self.start.start_address()), + // @fixme end is calculated incorrectly, add test + end: Page::containing_address(self.end.start_address()), + } + } +} + +impl fmt::Debug for PageRange { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + f.debug_struct("PageRange") + .field("start", &self.start) + .field("end", &self.end) + .finish() + } +} + +/// A range of pages with inclusive upper bound. +#[derive(Clone, Copy, PartialEq, Eq)] +pub struct PageRangeInclusive { + /// The start of the range, inclusive. + pub start: Page, + /// The end of the range, inclusive. + pub end: Page, +} + +impl PageRangeInclusive { + /// Returns whether this range contains no pages. + pub fn is_empty(&self) -> bool { + self.start > self.end + } +} + +impl Iterator for PageRangeInclusive { + type Item = Page; + + fn next(&mut self) -> Option { + if !self.is_empty() { + let page = self.start; + self.start += S::SIZE; + Some(page) + } else { + None + } + } +} + +impl fmt::Debug for PageRangeInclusive { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + f.debug_struct("PageRangeInclusive") + .field("start", &self.start) + .field("end", &self.end) + .finish() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test_case] + pub fn test_page_ranges() { + let page_size = Size4KiB::SIZE; + let number = 1000usize; + + let start_addr = VirtAddr::new(0xdeafbead); + let start: Page = Page::::containing_address(start_addr); + let end = start + number * page_size; + + let mut range = Page::range(start, end); + for i in 0..number { + assert_eq!( + range.next(), + Some(Page::containing_address(start_addr + page_size * i)) + ); + } + assert_eq!(range.next(), None); + + let mut range_inclusive = Page::range_inclusive(start, end); + for i in 0..=number { + assert_eq!( + range_inclusive.next(), + Some(Page::containing_address(start_addr + page_size * i)) + ); + } + assert_eq!(range_inclusive.next(), None); + } + + #[test_case] + fn test_page_range_conversion() { + let page_size = Size2MiB::SIZE; + let number = 10; + + let start_addr = VirtAddr::new(0xdeadbeaf); + let start = Page::::containing_address(start_addr); + let end = start + number * page_size; + + let range = Page::::range(start, end); + assert_eq!(range.num_pages(), 10); + + let range = range.as_4kib_page_range(); + // 10 2MiB pages is 5120 4KiB pages + assert_eq!(range.num_pages(), 5120); + } +} diff --git a/libs/memory/src/phys_addr.rs b/libs/memory/src/phys_addr.rs index dd1021142..2ef9bd1df 100644 --- a/libs/memory/src/phys_addr.rs +++ b/libs/memory/src/phys_addr.rs @@ -4,7 +4,10 @@ */ use { - crate::mm::{align_down, align_up}, + crate::{ + memory::VirtAddr, + mm::{align_down, align_up}, + }, bit_field::BitField, core::{ convert::From, @@ -98,6 +101,13 @@ impl PhysAddr { pub fn is_aligned(self, align: usize) -> bool { self.aligned_down(align) == self } + + /// Convert physical memory address into a kernel virtual address. + pub fn user_to_kernel(&self) -> VirtAddr { + use super::PHYSICAL_MEMORY_OFFSET; + assert!(self.0 < !PHYSICAL_MEMORY_OFFSET); // Can't have phys address over 1GiB then + VirtAddr::new(self.0 + PHYSICAL_MEMORY_OFFSET) + } } impl fmt::Debug for PhysAddr { diff --git a/libs/memory/src/platform/raspberrypi/memory/mmu.rs b/libs/memory/src/platform/raspberrypi/memory/mmu.rs index 06d4000cb..5437e816d 100644 --- a/libs/memory/src/platform/raspberrypi/memory/mmu.rs +++ b/libs/memory/src/platform/raspberrypi/memory/mmu.rs @@ -2,11 +2,11 @@ use { crate::{ - Physical, Virtual, mmu::{ self as generic_mmu, AccessPermissions, AddressSpace, AssociatedTranslationTable, AttributeFields, MemAttributes, MemoryRegion, PageAddress, TranslationGranule, }, + Physical, Virtual, }, liblocking::InitStateLock, }; @@ -108,6 +108,8 @@ fn kernel_virt_to_phys_region(virt_region: MemoryRegion) -> MemoryRegio // Subsumed by the kernel_map_binary() function //-------------------------------------------------------------------------------------------------- +// These are part of a static linked image and used for proper kernel-space initialization. +// i.e. these data are subtracted from the dtb-provided memory map. // pub static LAYOUT: KernelVirtualLayout = KernelVirtualLayout::new( // memory_map::END_INCLUSIVE, // [ @@ -123,6 +125,7 @@ fn kernel_virt_to_phys_region(virt_region: MemoryRegion) -> MemoryRegio // execute_never: true, // }, // }, +// @todo these should come from DTB and mem-map? // TranslationDescriptor { // name: "Device MMIO", // virtual_range: mmio_range_inclusive, @@ -133,6 +136,7 @@ fn kernel_virt_to_phys_region(virt_region: MemoryRegion) -> MemoryRegio // execute_never: true, // }, // }, +// @todo these should come from DTB and mem-map? // TranslationDescriptor { // name: "DMA heap pool", // virtual_range: dma_range_inclusive, diff --git a/libs/memory/src/virt_addr.rs b/libs/memory/src/virt_addr.rs index efb40db70..938ed3910 100644 --- a/libs/memory/src/virt_addr.rs +++ b/libs/memory/src/virt_addr.rs @@ -4,14 +4,17 @@ */ use { - crate::mm::{align_down, align_up}, + crate::{ + memory::PhysAddr, + mm::{align_down, align_up}, + }, bit_field::BitField, core::{ convert::{From, TryInto}, fmt, ops::{Add, AddAssign, Rem, RemAssign, Sub, SubAssign}, }, - usize_conversions::{FromUsize, usize_from}, + usize_conversions::{usize_from, FromUsize}, ux::*, }; @@ -34,7 +37,7 @@ pub struct VirtAddr(u64); /// a valid sign extension and are not null either. So automatic sign extension would have /// overwritten possibly meaningful bits. This likely indicates a bug, for example an invalid /// address calculation. -#[derive(Debug)] +#[derive(Debug, PartialEq, Eq, PartialOrd, Ord)] pub struct VirtAddrNotValid(u64); impl VirtAddr { @@ -159,6 +162,13 @@ impl VirtAddr { pub fn l0_index(&self) -> u9 { u9::new(((self.0 >> 12 >> 9 >> 9 >> 9) & 0o777).try_into().unwrap()) } + + /// Convert kernel-space virtual address into a physical memory address. + pub fn kernel_to_user(&self) -> PhysAddr { + use super::PHYSICAL_MEMORY_OFFSET; + assert!(self.0 > PHYSICAL_MEMORY_OFFSET); + PhysAddr::new(self.0 - PHYSICAL_MEMORY_OFFSET) + } } impl fmt::Debug for VirtAddr { @@ -203,92 +213,56 @@ impl From for u64 { } } -impl Add for VirtAddr { +impl Add for VirtAddr { type Output = Self; - fn add(self, rhs: u64) -> Self::Output { - VirtAddr::new(self.0 + rhs) + /// Add a given offset to the current virtual address. Never wraps. + fn add(self, rhs: T) -> Self::Output { + // @todo runtime cost of unwrap() here + VirtAddr::new(self.0.saturating_add(num::cast(rhs).unwrap())) } } -impl AddAssign for VirtAddr { - fn add_assign(&mut self, rhs: u64) { +impl AddAssign for VirtAddr { + fn add_assign(&mut self, rhs: T) { *self = *self + rhs; } } -impl Add for VirtAddr -where - u64: FromUsize, -{ +impl Sub for VirtAddr { type Output = Self; - fn add(self, rhs: usize) -> Self::Output { - self + u64::from_usize(rhs) + /// Subtract a given offset from the current virtual address. Never wraps. + fn sub(self, rhs: T) -> Self::Output { + // @todo runtime cost of unwrap() here + VirtAddr::new(self.0.saturating_sub(num::cast(rhs).unwrap())) } } -impl AddAssign for VirtAddr -where - u64: FromUsize, -{ - fn add_assign(&mut self, rhs: usize) { - self.add_assign(u64::from_usize(rhs)); - } -} - -impl Sub for VirtAddr { - type Output = Self; - fn sub(self, rhs: u64) -> Self::Output { - VirtAddr::new(self.0.checked_sub(rhs).unwrap()) - } -} - -impl SubAssign for VirtAddr { - fn sub_assign(&mut self, rhs: u64) { +impl SubAssign for VirtAddr { + fn sub_assign(&mut self, rhs: T) { *self = *self - rhs; } } -impl Sub for VirtAddr -where - u64: FromUsize, -{ - type Output = Self; - fn sub(self, rhs: usize) -> Self::Output { - self - u64::from_usize(rhs) - } -} - -impl SubAssign for VirtAddr -where - u64: FromUsize, -{ - fn sub_assign(&mut self, rhs: usize) { - self.sub_assign(u64::from_usize(rhs)); - } -} - -impl Sub for VirtAddr { +impl Sub for VirtAddr { type Output = u64; + /// Produce a difference between two virtual addresses. fn sub(self, rhs: VirtAddr) -> Self::Output { - self.as_u64().checked_sub(rhs.as_u64()).unwrap() + self.as_u64().checked_sub(rhs.as_u64()).unwrap() // @todo use i64? } } -impl Rem for VirtAddr -where - u64: FromUsize, -{ +impl Rem for VirtAddr { type Output = u64; - fn rem(self, rhs: usize) -> Self::Output { - self.0 % u64::from_usize(rhs) + fn rem(self, rhs: T) -> Self::Output { + num::traits::CheckedRem::checked_rem(&self.0, &num::cast(rhs).unwrap()).unwrap() } } -impl RemAssign for VirtAddr -where - u64: FromUsize, -{ - fn rem_assign(&mut self, rhs: usize) { - *self = VirtAddr::new(self.0 % u64::from_usize(rhs)); +// @todo this is not very useful... +impl RemAssign for VirtAddr { + fn rem_assign(&mut self, rhs: T) { + *self = VirtAddr::new( + num::traits::CheckedRem::checked_rem(&self.0, &num::cast(rhs).unwrap()).unwrap(), + ); } } diff --git a/nucleus/Cargo.toml b/nucleus/Cargo.toml index 7f8f63312..b46761366 100644 --- a/nucleus/Cargo.toml +++ b/nucleus/Cargo.toml @@ -47,6 +47,7 @@ libplatform = { workspace = true } libqemu = { workspace = true, optional = true } libtime = { workspace = true } snafu = { workspace = true } +static_assertions = { workspace = true } tock-registers = { workspace = true } usize_conversions = { workspace = true } ux = { workspace = true } diff --git a/nucleus/src/main.rs b/nucleus/src/main.rs index 2b78dc35f..27bfff904 100644 --- a/nucleus/src/main.rs +++ b/nucleus/src/main.rs @@ -14,6 +14,9 @@ #![feature(format_args_nl)] #![feature(stmt_expr_attributes)] #![feature(slice_ptr_get)] +#![feature(default_free_fn)] +#![feature(const_fn_trait_bound)] +#![feature(nonnull_slice_from_raw_parts)] #![deny(missing_docs)] #![deny(warnings)] #![allow(unused)] @@ -24,7 +27,7 @@ use core::panic::PanicInfo; #[allow(unused_imports)] -use libconsole::{SerialOps, console::console}; +use libconsole::{console::console, SerialOps}; use { cfg_if::cfg_if, core::{cell::UnsafeCell, time::Duration}, @@ -108,6 +111,8 @@ pub fn kernel_main() -> ! { // info!("MMU online. Special regions:"); // machine::platform::memory::mmu::virt_mem_layout().print_layout(); + dump_memory_map(); + let (_, privilege_level) = libexception::exception::current_privilege_level(); info!("Current privilege level: {privilege_level}"); @@ -144,8 +149,13 @@ fn panicked(info: &PanicInfo) -> ! { } fn print_mmu_state_and_features() { - // use machine::memory::mmu::interface::MMU; - // memory::mmu::mmu().print_features(); + memory::features::print_features(); +} + +fn dump_memory_map() { + // Output the memory map as we could derive from FDT and information about our loaded image + // Use it to imagine how the memmap would look like in the end. + arch::memory::print_layout(); } //------------------------------------------------------------ diff --git a/nucleus/tests/memory.rs b/nucleus/tests/memory.rs index 1dbf8dfd1..b8cab6081 100644 --- a/nucleus/tests/memory.rs +++ b/nucleus/tests/memory.rs @@ -19,21 +19,21 @@ use { liblocking::interface::Mutex, liblog::println, libmemory::{ - Address, - Physical, - Virtual, arch::mmu::translation_table::{PageDescriptor, TableDescriptor}, - mm::{BumpAllocator, align_up}, + mm::{align_up, BumpAllocator}, mmu::{ - AccessPermissions, AttributeFields, MemAttributes, MemoryRegion, PageAddress, kernel_map_at, page_alloc, - translation_table::{FixedSizeTranslationTable, interface::TranslationTable}, + translation_table::{interface::TranslationTable, FixedSizeTranslationTable}, + AccessPermissions, AttributeFields, MemAttributes, MemoryRegion, PageAddress, }, phys_addr::{PhysAddr, PhysAddrNotValid}, - platform::KernelGranule, //memory::mmu::KernelGranule}, platform::memory::mmu::{ - KERNEL_TABLES, virt_boot_core_stack_region, virt_code_region, virt_data_region, + virt_boot_core_stack_region, virt_code_region, virt_data_region, KERNEL_TABLES, }, + platform::KernelGranule, //memory::mmu::KernelGranule}, + Address, + Physical, + Virtual, }, }; @@ -87,7 +87,7 @@ fn test_allocates_within_init_range() { assert!(result3.is_err()); } -// Creating with end <= start sshould fail +// Creating with end <= start should fail // @todo return Result<> from new? #[test_case] fn test_bad_allocator() { diff --git a/play/memtab.rs b/play/memtab.rs new file mode 100644 index 000000000..99da956e8 --- /dev/null +++ b/play/memtab.rs @@ -0,0 +1,520 @@ +//# snafu = "*" +// Explore memory table abstractions +// +// +#[allow(unused_imports)] +use { + core::{ + marker::PhantomData, + ops::{Index, IndexMut}, + }, + snafu::Snafu, +}; + +struct FrameAllocator { + counter: usize; +} + +impl FrameAllocator { + fn new() -> Self { + Self {counter:0} + } + // Return a frame allocated from physical memory. + // Frame physical address is mapped to kernel space. + fn grab_frame(&mut self) -> G::Frame { + let phys_base = self.allocate_phys_frame(); + Frame::new(phys_base.phys_to_kernel()) + } + + fn allocate_phys_frame(&mut self) -> usize { + self.counter += 1; + self.counter * G::Size + } +} + +struct Frame(usize); + +impl Frame { + pub fn new(base: usize) -> Self { + Self(base) + } +} + +fn main() { + println!("Hello, play"); + + // Build page table hierarchy from Stage1 down to Stage4. + let allocator = FrameAllocator::new(); + + // Using Granule4k: + let l0_table = 0; + + // Using Granule16k: + let l0_table = 0; + + // Using Granule64k: + let l0_table = Stage1::::new(); + let arena = allocator.grab_frame::(); // give parent table here, to extract granule + let l1_table = l0_table.allocate_page_from(arena); + let arena = allocator.grab_frame::(); + let l2_table = l1_table.allocate_page_from(arena); + + //================= + // TRAVERSE TABLES + //================= + + let base_addr = 124467000usize; // this comes from TTBRx register or some table base variable for each process. + + // everything starts with a translation table base address + let sys_l0_table = base_addr as *const Stage1; + + // Access the table using virtual addresses (each stage consumes more bits of the address). + let l0_table = sys_l0_table; + let l1 = l0_table[virt_addr]; + let l2 = l1_table[virt_addr]; + let phys = l2_table[virt_addr]; + + // to access physical memory from kernel + let phys = 123456usize; + let kern_phys = phys.phys_to_kernel(); +} + +trait PhysicalKernelMapping { + type PhysAddr; + fn phys_to_kernel(&self) -> Self::PhysAddr; + fn kernel_to_phys(&self) -> Self::PhysAddr; +} + +const KERNEL_PHYS_MAP_BASE: usize = 0xffff_fff0_0000_0000; // Not const, but defined by available RAM size + +impl PhysicalKernelMapping for usize { + type PhysAddr = usize; + #[inline(always)] + fn phys_to_kernel(&self) -> usize { + self.checked_add(KERNEL_PHYS_MAP_BASE) + .expect("Physical to kernel mapping overflowed") + } + #[inline(always)] + fn kernel_to_phys(&self) -> usize { + self.checked_sub(KERNEL_PHYS_MAP_BASE) + .expect("Kernel to physical mapping underflowed") + } +} + +// impl indexing stages by virtual address only, +// so l0[virt][virt][virt][virt] will go through all levels of translation +// + +impl Index for Stage1 { + type Output = Self::NextStage; + + fn index(&self, index: Virtual) -> Self::NextStage {} +} + +type Physical = u64; +type Virtual = u64; + +trait Stage { + type G: Granule; + type NextStage; +} + +trait Descriptor { + type GRANULE: Granule; + type STAGE: Stage; // Get NextStage from this STAGE + fn is_leaf() -> bool; +} + +trait NextLevelDescriptor: Descriptor { + fn get_next_level_desciptor() -> impl Descriptor; +} + +trait LeafDescriptor: Descriptor { + fn get_translated_address() -> Physical; +} + +/// Abstract over possible granule sizes. +trait Granule { + // Mask and shift to extract entry index from virt address + const MASK_BITS: usize; + const MASK: u64 = 1 << Self::MASK_BITS - 1; + const SHIFT: usize; + fn get_index(address: Virtual) -> usize { + return ((address >> Self::SHIFT) & Self::MASK) + .try_into() + .expect("Arithmetics gone mad"); + } +} + +/// Abstract over the possible page sizes, 4KiB, 16KiB, 2MiB, 1GiB. +/// ... .arch independent page sizes actually, so not linked to granules or anything... +/// Page sizes depend on stage and granule size - could be auto-derived? +pub trait PageSize: Copy + PartialEq + Eq + PartialOrd + Ord { + /// A string representation of the page size for debug output. + const SIZE_AS_DEBUG_STR: &'static str; + + /// The page shift in bits. + const SHIFT: usize; + + /// The page size in bytes. + const SIZE: usize = 1 << Self::SHIFT; + + /// The page size mask in bits. + const MASK: u64 = 1 << Self::SHIFT - 1; + + fn alignment() -> usize { + Self::SIZE + } + + fn mask() -> u64 { + Self::MASK + } +} + +/// This trait is implemented for 4KiB, 16KiB, and 2MiB pages, but not for 1GiB pages. +/// This trait is actually not necessary - do the BlockSize trait instead. +trait NotGiantPageSize: PageSize {} + +/// Marker for granule sizes impls. +trait GranuleSize {} + +/// Stages are parameterised by the used granule size. This determines their max size and resolution step. +/// It also determines the return type (table/block/page) of the resolution step? hmm. +/// (use an assoc type to resolve it e.g. trait NextStage { type Resolution; fn resolve() -> Self::Resolution; }) +struct Stage1 { + _granule: PhantomData, +} +struct Stage2 { + _granule: PhantomData, +} +struct Stage3 { + _granule: PhantomData, +} +struct Stage4 { + _granule: PhantomData, +} + +impl Stage1 { + pub fn new() -> Self { + Self { + _granule: PhantomData, + } + } +} + +trait NextStage { + type BaseTable; // not Stage1..4 b/c we can't pass any stage to a stage 1 + type Resolution; + fn resolve(_table: &Self::BaseTable) -> Self::Resolution; +} + +// @todo: impl_granule! macro? +impl NextStage for Stage1 { + type BaseTable = u64; // ? + type Resolution = u64; // ? + fn resolve(_table: &Self::BaseTable) -> Self::Resolution { + 0u64 + } +} +impl NextStage for Stage1 { + type BaseTable = u64; // ? + type Resolution = u64; // ? + fn resolve(_table: &Self::BaseTable) -> Self::Resolution { + 0u64 + } +} +impl NextStage for Stage1 { + type BaseTable = u64; // ? + type Resolution = u64; // ? + fn resolve(_table: &Self::BaseTable) -> Self::Resolution { + 0u64 + } +} + +impl NextStage for Stage2 { + type BaseTable = u64; // ? + type Resolution = u64; // ? + fn resolve(_table: &Self::BaseTable) -> Self::Resolution { + 0u64 + } +} +impl NextStage for Stage2 { + type BaseTable = u64; // ? + type Resolution = u64; // ? + fn resolve(_table: &Self::BaseTable) -> Self::Resolution { + 0u64 + } +} +impl NextStage for Stage2 { + type BaseTable = u64; // ? + type Resolution = u64; // ? + fn resolve(_table: &Self::BaseTable) -> Self::Resolution { + 0u64 + } +} + +impl NextStage for Stage3 { + type BaseTable = u64; // ? + type Resolution = u64; // ? + fn resolve(_table: &Self::BaseTable) -> Self::Resolution { + 0u64 + } +} +impl NextStage for Stage3 { + type BaseTable = u64; // ? + type Resolution = u64; // ? + fn resolve(_table: &Self::BaseTable) -> Self::Resolution { + 0u64 + } +} +impl NextStage for Stage3 { + type BaseTable = u64; // ? + type Resolution = u64; // ? + fn resolve(_table: &Self::BaseTable) -> Self::Resolution { + 0u64 + } +} + +impl NextStage for Stage4 { + type BaseTable = u64; // ? + type Resolution = u64; // ? + fn resolve(_table: &Self::BaseTable) -> Self::Resolution { + 0u64 + } +} +impl NextStage for Stage4 { + type BaseTable = u64; // ? + type Resolution = u64; // ? + fn resolve(_table: &Self::BaseTable) -> Self::Resolution { + 0u64 + } +} +impl NextStage for Stage4 { + type BaseTable = u64; // ? + type Resolution = u64; // ? + fn resolve(_table: &Self::BaseTable) -> Self::Resolution { + 0u64 + } +} + +// Stage 1 always points to more tables +// impl NextLevelDescriptor for Stage1 { +// fn get_next_level_descriptor(table: &AssociatedTranslationTable) -> impl Descriptor { +// // check validity bits +// // return next descriptor from the associated table +// } +// } + +// impl NextLevelDescriptor for Stage2 {} + +// impl NextLevelDescriptor for Stage3 {} + +// impl LeafDescriptor for Stage4 {} + +type TraverseResult = Result; +enum Output { + Table(TableDescriptor), // duh need to put next table type in here ... + Block(BlockDescriptor), +} + +type ulog2 = usize; + +struct BlockDescriptor { + base_addr: PhysAddr, + size: ulog2, // should give a mask to combine with block offset from virt_addr +} + +trait TableOnly { + type NextTable; + fn next_table(&self, virt: VirtAddr) -> Self::NextTable; +} + +trait BlockOnly { + type Block; + fn block(&self, virt: VirtAddr) -> Self::Block; +} + +trait TableOrBlock: TableOnly + BlockOnly {} + +impl TableOnly for Stage1 { + type NextTable = TraverseResult; + fn next_table(&self, virt: VirtAddr) -> Self::NextTable { + Err(TraverseError::NotPresent) + } +} + +impl TableOnly for Stage2 { + type NextTable = TraverseResult; + fn next_table(&self, virt: VirtAddr) -> Self::NextTable { + Err(TraverseError::NotPresent) + } +} + +impl BlockOnly for Stage2 { + type Block = TraverseResult; + fn block(&self, virt: VirtAddr) -> Self::Block { + let index = virt >> Self::SHIFT & (1 << Self::MASK_BITS) - 1; + Err(TraverseError::NotPresent) + } +} + +impl TableOrBlock for Stage2 {} + +// +// Probably implement it as Granules vs Stages? +// Some systems may support variable granule sizes, some may not. +// Each Stage/Granule combination will yield bitmasks and/or bitfield! references +// and/or types of further stages (shall return enums?) +// + +// Specific granules + +struct Granule4k; +struct Granule16k; +struct Granule64k; + +impl GranuleSize for Granule4k {} +impl GranuleSize for Granule16k {} +impl GranuleSize for Granule64k {} + +// Masks to extract entry index from virt address for different stages and granule sizes +impl Granule for Stage1 { + const MASK_BITS: usize = 9; + const SHIFT: usize = 39; +} +impl Granule for Stage1 { + const MASK_BITS: usize = 1; + const SHIFT: usize = 47; +} +impl Granule for Stage1 { + const MASK_BITS: usize = 5; + const SHIFT: usize = 42; +} + +//------------------------ +// Page: with 4kb granule +//------------------------ + +/// A standard 4KiB page. +#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)] +pub enum Size4KiB {} + +impl PageSize for Size4KiB { + const SIZE_AS_DEBUG_STR: &'static str = "4KiB"; + const SHIFT: usize = 12; +} + +impl NotGiantPageSize for Size4KiB {} + +//------------------------- +// Page: with 16kb granule +//------------------------- + +/// A standard 16KiB page. +#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)] +pub enum Size16KiB {} + +impl PageSize for Size16KiB { + const SIZE_AS_DEBUG_STR: &'static str = "16KiB"; + const SHIFT: usize = 14; +} + +impl NotGiantPageSize for Size16KiB {} + +//------------------------- +// Page: with 64kb granule +//------------------------- + +/// A standard 64KiB page. +#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)] +pub enum Size64KiB {} + +impl PageSize for Size64KiB { + const SIZE_AS_DEBUG_STR: &'static str = "64KiB"; + const SHIFT: usize = 16; +} + +impl NotGiantPageSize for Size64KiB {} + +//-------------------------- +// Blocks: with 4kb granule +//-------------------------- + +/// A “huge” 2MiB page. +#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)] +pub enum Size2MiB {} + +impl PageSize for Size2MiB { + const SIZE_AS_DEBUG_STR: &'static str = "2MiB"; + const SHIFT: usize = 21; +} + +impl NotGiantPageSize for Size2MiB {} + +/// A “giant” 1GiB page. +#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)] +pub enum Size1GiB {} + +impl PageSize for Size1GiB { + const SIZE_AS_DEBUG_STR: &'static str = "1GiB"; + const SHIFT: usize = 30; +} + +//--------------------------- +// Blocks: with 16kb granule +//--------------------------- + +#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)] +pub enum Size32MiB {} + +impl PageSize for Size32MiB { + const SIZE_AS_DEBUG_STR: &'static str = "32MiB"; + const SHIFT: usize = 25; +} + +impl NotGiantPageSize for Size32MiB {} + +//--------------------------- +// Blocks: with 64kb granule +//--------------------------- + +#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)] +pub enum Size512MiB {} + +impl PageSize for Size512MiB { + const SIZE_AS_DEBUG_STR: &'static str = "512MiB"; + const SHIFT: usize = 29; +} + +impl NotGiantPageSize for Size512MiB {} + +/// The error returned by the `PageTableEntry::frame` method. +#[derive(Snafu, Debug, Clone, Copy, PartialEq)] +pub enum TraverseError { + /// The entry does not have the `PRESENT` flag set, so it isn't currently mapped to a frame. + NotPresent, +} + +// maestro: +// Calls `alloc` with order `order`. +// +// The allocated frame is in the kernel zone. +// +// The function returns the *virtual* address, not the physical one. +// pub fn alloc_kernel(order: FrameOrder) -> AllocResult> { +// let ptr = alloc(order, FLAG_ZONE_TYPE_KERNEL)?; +// let virt_ptr = memory::kern_to_virt(ptr.as_ptr()) as _; +// debug_assert!(virt_ptr as *const _ >= memory::PROCESS_END); +// NonNull::new(virt_ptr).ok_or(AllocError) +// } + +// Allocates a paging object and returns its virtual address. +// +// If the allocation fails, the function returns an error. +// fn alloc_obj() -> AllocResult<*mut u32> { +// let mut ptr = buddy::alloc_kernel(0)?.cast::(); +// // Zero memory +// let slice = unsafe { slice::from_raw_parts_mut(ptr.as_mut(), buddy::get_frame_size(0)) }; +// slice.fill(0); +// Ok(ptr.as_ptr() as _) +// } diff --git a/play/memtab2.rs b/play/memtab2.rs new file mode 100644 index 000000000..ced54759b --- /dev/null +++ b/play/memtab2.rs @@ -0,0 +1,520 @@ +//# snafu = "*" +//# either = "*" +// +// Explore memory table abstractions +// +// #![feature(decl_macro)] +#![feature(allocator_api)] +#![allow(unused)] +#![allow(unused_imports)] +use { + core::{ + marker::PhantomData, + ops::{Index, IndexMut}, + }, + either::*, + snafu::{ResultExt, Snafu}, + std::alloc::{alloc_zeroed, dealloc, Layout}, +}; + +type VirtAddr = u64; +type PhysAddr = u64; + +/// Provides means to extract the next table level index or the block address. +trait TableIndex { + // Mask and shift to extract entry index from virt address + const MASK_BITS: usize; + const MASK: u64 = 1 << Self::MASK_BITS - 1; + const SHIFT: usize; + const BLOCK_ADDR_MASK: u64; + const TABLE_ADDR_MASK: u64; + fn extract_index(address: VirtAddr) -> usize { + return ((address >> Self::SHIFT) & Self::MASK) + .try_into() + .expect("Arithmetics gone mad"); + } + // Extract address of a block with appropriate size (used by BlockOnly) + fn extract_block_base(entry: u64) -> u64 { + entry & Self::BLOCK_ADDR_MASK + } + // Extract address of the next table (use by TableOnly) -- aligned to the granule size for VMSAv8 + fn extract_table_base(entry: u64) -> u64 { + entry & Self::TABLE_ADDR_MASK + } +} + +// pub enum TraverseError { + +#[derive(Debug, Clone, Copy, PartialEq)] // Snafu +enum TableError { + /// The entry does not have the `PRESENT` flag set, so it isn't currently mapped to a frame. + NotPresent, +} + +trait TableOnly { + type NextTable; + fn next_table(&self, virt: VirtAddr) -> Result<&mut Self::NextTable, TableError>; +} + +#[derive(Debug, Clone, Copy, PartialEq)] // Snafu +enum BlockError { + /// The entry does not have the `PRESENT` flag set, so it isn't currently mapped to a frame. + NotPresent, +} + +trait BlockOnly { + type Block; + fn block(&self, virt: VirtAddr) -> Result; +} + +trait TableOrBlock { + type NextTable; + fn next_table(&self, virt: VirtAddr) -> Result<&mut Self::NextTable, TableError>; + type Block; + fn block(&self, virt: VirtAddr) -> Result; +} + +// @todo next() should also take a VirtAddr? +// trait NextStage: TableOrBlock { +// fn next( +// &self, +// ) -> Either, dyn BlockOnly>; +// } + +macro_rules! make_stage { + { name:$ident } => { + #[allow(non_camel_case_types)] + struct $name { + entries: [u64; 1 << Self::MASK_BITS], // u64 must be more flexible type EntryT for other arch's + } + } +} + +// As you can see, the Stage structures are repeated, as are the granules (masks), +// so we need to be able to parameterize them both somehow +// e.g. mask depends on both stage and granule +// These stage tables (or PageTable really) can be implemented in terms of TableIndex::MASK_BITS +// e.g. struct Stage { pub entries: [u64; 1 << I::MASK_BITS]; } +// Specific table structures: + +make_stage!(Stage1_Gran4k); +make_stage!(Stage2_Gran4k); +make_stage!(Stage3_Gran4k); +make_stage!(Stage4_Gran4k); + +make_stage!(Stage1_Gran16k); +make_stage!(Stage2_Gran16k); +make_stage!(Stage3_Gran16k); +make_stage!(Stage4_Gran16k); + +make_stage!(Stage1_Gran64k); +make_stage!(Stage2_Gran64k); +make_stage!(Stage3_Gran64k); + +macro_rules! impl_table_index { + { $stage:ty, index = $mask_bits:literal @ $shift:literal, table = $table_bits:literal @ $table_shift:literal, block = $block_bits:literal @ $block_shift:literal } => { + impl TableIndex for $stage { + const MASK_BITS: usize = $mask_bits; + const SHIFT: usize = $shift; + const BLOCK_ADDR_MASK: u64 = ((1 << $block_bits) - 1) << $block_shift; + const TABLE_ADDR_MASK: u64 = ((1 << $table_bits) - 1) << $table_shift; + } + } +} + +impl_table_index!(Stage1_Gran4k, index = 9@39, table = 36@12, block = 0@0); +impl_table_index!(Stage2_Gran4k, index = 9@30, table = 36@12, block = 18@30); +impl_table_index!(Stage3_Gran4k, index = 9@21, table = 36@12, block = 27@21); +impl_table_index!(Stage4_Gran4k, index = 9@12, table = 36@12, block = 36@12); + +impl_table_index!(Stage1_Gran16k, index = 1@47, table = 34@14, block = 0@0); +impl_table_index!(Stage2_Gran16k, index = 11@36, table = 34@14, block = 12@36); +impl_table_index!(Stage3_Gran16k, index = 11@25, table = 34@14, block = 23@25); +impl_table_index!(Stage4_Gran16k, index = 11@14, table = 34@14, block = 34@14); + +impl_table_index!(Stage1_Gran64k, index = 5@42, table = 32@16, block = 5@42); // 4TiB block! +impl_table_index!(Stage2_Gran64k, index = 13@29, table = 32@16, block = 19@29); +impl_table_index!(Stage3_Gran64k, index = 13@16, table = 32@16, block = 32@16); +// impl_table_index!(Stage4_Gran64k, index = 0@0); // N/A + +unsafe fn next_level(virt: VirtAddr) -> Result { + let index = <$stage>::extract_index(virt); + let entry = self.entries[index]; + if !bit_set(entry, P) { + return Err(TableError::NotPresent); + } + Ok(base) +} + +macro_rules! impl_table_only { + { $stage:ty, $next_stage:ty } => { + impl TableOnly for $stage where $stage: TableIndex { + type NextTable = $next_stage; + + fn next_table(&self, virt: VirtAddr) -> Result<&mut Self::NextTable, TableError> { + let entry = next_level(virt); + let base = <$stage>::extract_table_base(entry); // This involves some stage-dependent shenanigans, e.g. for RISC-V the PN[x] entries must be combined depending on the stage. + let next_table = base as *mut Self::NextTable; // ptr.cast<>? + let next_table = unsafe { &mut *next_table }; + Ok(next_table) + } + } + } +} + +// temp: +const P: usize = 0x0; + +fn bit_set(field: u64, bit: usize) -> bool { + field & (1 << bit) != 0 +} + +macro_rules! impl_block_only { + { $stage:ty, $block_size:ty } => { + impl BlockOnly for $stage where $stage: TableIndex { + type Block = Frame<$block_size>; // we will get a block of given size from this stage + + fn block(&self, virt: VirtAddr) -> Result { + let entry = next_level(virt); + let phys_base = <$stage>::extract_block_base(entry); + let block = Frame::new(phys_base.try_into().expect("It fits fine!")); + Ok(block) + } + } + } +} + +// Granularity 4KiB +impl_table_only!(Stage1_Gran4k, Stage2_Gran4k); + +impl_table_only!(Stage2_Gran4k, Stage3_Gran4k); +impl_block_only!(Stage2_Gran4k, Size1GiB); + +impl_table_only!(Stage3_Gran4k, Stage4_Gran4k); +impl_block_only!(Stage3_Gran4k, Size2MiB); + +impl_block_only!(Stage4_Gran4k, Size4KiB); + +// Granularity 16KiB +impl_table_only!(Stage1_Gran16k, Stage2_Gran16k); + +impl_table_only!(Stage2_Gran16k, Stage3_Gran16k); +impl_block_only!(Stage2_Gran16k, Size1GiB); + +impl_table_only!(Stage3_Gran16k, Stage4_Gran16k); +impl_block_only!(Stage3_Gran16k, Size2MiB); + +impl_block_only!(Stage4_Gran16k, Size4KiB); + +// Granularity 64KiB +impl_table_only!(Stage1_Gran64k, Stage2_Gran64k); +impl_block_only!(Stage1_Gran64k, Size4TiB); + +impl_table_only!(Stage2_Gran64k, Stage3_Gran64k); +impl_block_only!(Stage2_Gran64k, Size512MiB); + +impl_block_only!(Stage3_Gran64k, Size64KiB); + +macro_rules! impl_next_stage { + { $stage:ty, $next_stage:ty, $block:ty } => { + // @todo: Index for slicerange (restricted range! must maintain alignments) + // or range to fill in sequential table blocks on aarch64 + // e.g. stage4[0..15] = compound_large_segment; <- not using the VirtAddr here.. + impl Index for $stage + where + $stage: TableOnly, + { + type Output = Self::NextTable; + + fn index(&self, virt: VirtAddr) -> &Self::Output { + let tbl = <$stage as TableOnly>::next_table(virt); + &*tbl + } + } + + impl IndexMut for &mut $stage + where + $stage: TableOnly, + { + fn index_mut(&mut self, virt: VirtAddr) -> &mut Self::Output { + let index = Self::extract_index(virt); + Self::extract_table_base(self.entries[index]) + } + } + + impl Index for &$stage + where + $stage: BlockOnly, + { + type Output = ::Block; + + fn index(&self, virt: VirtAddr) -> &Self::Output { + let index = Self::extract_index(virt); + Self::extract_block_base(self.entries[index]) + } + } + + impl IndexMut for &mut $stage + where + $stage: BlockOnly, + { + // Should only return a mutable reference to entries[index] so that we + // could replace it. + fn index_mut(&mut self, virt: VirtAddr) -> &mut Self::Output { + let index = Self::extract_index(virt); + &mut self.entries[index] // but can't be just &mut u64, we need a proper table type? probably need to overload assignments too? + } + } + + // impl Index for T + // where + // T: TableIndex + TableOrBlock, + // { + // type Output = Either; + // fn index(&self, index: VirtAddr) -> &Self::Output { + // self.entries[index] + // } + // } + } +} + +// now for the TableOnly+BlockOnly combination we need to also provide a resolution method that returns either +impl_next_stage!(Stage1_Gran4k, Stage2_Gran4k, Size1GiB); // this provides next() -> Either +impl_next_stage!(Stage2_Gran4k, Stage3_Gran4k, Size2MiB); + +// @todo need to also provide next() for TableOnly and for BlockOnly types separately, without Either? + +// Level could be determining which of TableOnly, BlockOnly are implemented? (or implement Table/Block only FOR the Level?) +// Mask (TableIndex) gives extraction parameters for next table or next block. + +// Granule is not essential - it's arch specific and translates into number of Stages and +// TableIndex for ptr extraction. + +/// Abstract over the possible page sizes, 4KiB, 16KiB, 2MiB, 1GiB. +pub trait PageSize: Copy + PartialEq + Eq + PartialOrd + Ord { + /// A string representation of the page size for debug output. + const SIZE_AS_DEBUG_STR: &'static str; + + /// The page shift in bits. + const SHIFT: usize; + + /// The page size in bytes. + const SIZE: usize = 1 << Self::SHIFT; + + /// The page size mask in bits. + const MASK: u64 = 1 << Self::SHIFT - 1; + + fn alignment() -> usize { + Self::SIZE + } + + fn mask() -> u64 { + Self::MASK + } +} + +macro_rules! make_page { + { $name:ident, $debug_str:literal, $shift:literal } => { + #[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)] + pub enum $name {} + + impl PageSize for $name { + const SIZE_AS_DEBUG_STR: &'static str = $debug_str; + const SHIFT: usize = $shift; + } + } +} + +//------------------------ +// Page: with 4kb granule +//------------------------ + +// A standard 4KiB page. +make_page!(Size4KiB, "4KiB", 12); + +//------------------------- +// Page: with 16kb granule +//------------------------- + +// A standard 16KiB page. +make_page!(Size16KiB, "16KiB", 14); + +//------------------------- +// Page: with 64kb granule +//------------------------- + +// A standard 64KiB page. +make_page!(Size64KiB, "64KiB", 16); + +//-------------------------- +// Blocks: with 4kb granule +//-------------------------- + +// A “huge” 2MiB page. +make_page!(Size2MiB, "2MiB", 21); + +// A “giant” 1GiB page. +make_page!(Size1GiB, "1GiB", 30); + +//--------------------------- +// Blocks: with 16kb granule +//--------------------------- + +make_page!(Size32MiB, "32MiB", 25); + +//--------------------------- +// Blocks: with 64kb granule +//--------------------------- + +make_page!(Size512MiB, "512MiB", 29); + +make_page!(Size4TiB, "4TiB", 42); + +/// Physical page frame. +struct Frame { + base: usize, + _page_size: PhantomData

, +} + +impl Frame

{ + // @todo Check base is aligned to _page_size::SHIFT + pub fn new(base: usize) -> Self { + Self { + base, + _page_size: PhantomData, + } + } +} + +struct PageTableAllocator; +struct FrameAllocator; + +#[derive(Debug, Snafu)] +enum AllocError { + InvalidLayout { source: std::alloc::LayoutError }, +} + +impl PageTableAllocator { + fn alloc() -> Result<&mut T, AllocError> { + let size = (1 << T::MASK_BITS) * core::mem::size_of::(); + let layout = Layout::from_size_align(size, size).context(InvalidLayoutSnafu)?; + println!( + "Allocating page table: size {}, align {}", + layout.size(), + layout.align() + ); + let ptr = unsafe { alloc_zeroed(layout) }; + assert_ne!(ptr as usize, 0); + Ok(ptr as *mut T) + } + fn dealloc(ptr: *mut T) -> Result<(), AllocError> { + let size = (1 << T::MASK_BITS) * core::mem::size_of::(); + let layout = Layout::from_size_align(size, size).context(InvalidLayoutSnafu)?; + unsafe { dealloc(ptr as *mut u8, layout) }; + Ok(()) + } +} + +impl FrameAllocator { + fn alloc() -> Result, AllocError> { + let layout = + Layout::from_size_align(1 << P::SHIFT, 1 << P::SHIFT).context(InvalidLayoutSnafu)?; + println!( + "Allocating frame: size {}, align {}", + layout.size(), + layout.align() + ); + let ptr = unsafe { alloc_zeroed(layout) }; + assert_ne!(ptr as usize, 0); + Ok(Frame::new(ptr as usize)) + } + fn dealloc(ptr: Frame

) -> Result<(), AllocError> { + let layout = + Layout::from_size_align(1 << P::SHIFT, 1 << P::SHIFT).context(InvalidLayoutSnafu)?; + unsafe { dealloc(ptr.base as *mut u8, layout) }; + Ok(()) + } +} + +fn main() -> Result<(), AllocError> { + println!("Hello, play"); + + println!("Stage1_Gran4k {}", core::mem::size_of::()); + println!("Stage2_Gran4k {}", core::mem::size_of::()); + println!("Stage3_Gran4k {}", core::mem::size_of::()); + println!("Stage4_Gran4k {}", core::mem::size_of::()); + + println!("Stage1_Gran16k {}", core::mem::size_of::()); + println!("Stage2_Gran16k {}", core::mem::size_of::()); + println!("Stage3_Gran16k {}", core::mem::size_of::()); + println!("Stage4_Gran16k {}", core::mem::size_of::()); + + println!("Stage1_Gran64k {}", core::mem::size_of::()); + println!("Stage2_Gran64k {}", core::mem::size_of::()); + println!("Stage3_Gran64k {}", core::mem::size_of::()); + println!("Stage4_Gran64k {}", core::mem::size_of::()); + + // Build page table hierarchy from Stage1 down to Stage4. + let virt: VirtAddr = 0xdead_beef; + + // Using Granule4k: + let s1_table = PageTableAllocator::alloc::()?; + let s2_table = PageTableAllocator::alloc::()?; + s1_table[virt] = s2_table; + let s3_table = PageTableAllocator::alloc::()?; + s2_table[virt] = s3_table; + let s4_table = PageTableAllocator::alloc::()?; + s3_table[virt] = s4_table; + let leaf = FrameAllocator::alloc::()?; + s4_table[virt] = leaf; + + // Using Granule16k: + let mut s1_table = PageTableAllocator::alloc::()?; + let mut s2_table = PageTableAllocator::alloc::()?; + s1_table[virt] = s2_table; + let mut s3_table = PageTableAllocator::alloc::()?; + s2_table[virt] = s3_table; + let mut s4_table = PageTableAllocator::alloc::()?; + s3_table[virt] = s4_table; + let leaf = FrameAllocator::alloc::()?; + s4_table[virt] = leaf; + + // This should fail to compile (accidentally wrong stage granule) + let mut s3_table = PageTableAllocator::alloc::()?; + s2_table[virt] = s3_table; + + // This too (accidentally wrong table level) + let s3_table = PageTableAllocator::alloc::()?; + s2_table[virt] = s3_table; + + // // Using Granule64k: + let s1_table = PageTableAllocator::alloc::()?; + let s2_table = PageTableAllocator::alloc::()?; + s1_table[virt] = s2_table; + let s3_table = PageTableAllocator::alloc::()?; + s2_table[virt] = s3_table; + let leaf = FrameAllocator::alloc::()?; + s3_table[virt] = leaf; + + //================= + // TRAVERSE TABLES + //================= + + let virt: VirtAddr = 0xdead_beef; + + // let tt_base_addr = 124467000usize; // this comes from TTBRx register or some table base variable for each process. + + // // everything starts with a translation table base address + // let sys_l0_table = base_addr as *const Stage1; + + // // Access the table using virtual addresses (each stage consumes more bits of the address). + // let l0_table = sys_l0_table; + // let l1 = l0_table[virt_addr]; + // let l2 = l1_table[virt_addr]; + // let phys = l2_table[virt_addr]; + + // // to access physical memory from kernel + // let phys = 123456usize; + // let kern_phys = phys.phys_to_kernel(); + Ok(()) +} From 927aa67b45b42887572dc713548bada00607bd4d Mon Sep 17 00:00:00 2001 From: Berkus Decker Date: Sun, 18 Jan 2026 19:36:41 +0200 Subject: [PATCH 006/107] wip: buildable --- libs/console/Cargo.toml | 1 + libs/console/src/write_to.rs | 1 + libs/driver/Cargo.toml | 1 + libs/kernel-state/Cargo.toml | 1 + libs/local-irq/Cargo.toml | 1 + libs/locking/Cargo.toml | 1 + libs/log/Cargo.toml | 1 + libs/log/src/lib.rs | 2 +- libs/memory/docs/paging.puml | 40 ++++++ libs/memory/src/arch/aarch64/mmu/mod.rs | 8 +- .../src/arch/aarch64/mmu/translation_table.rs | 61 ++++---- .../src/platform/raspberrypi/linker/kernel.ld | 19 +++ nucleus/src/boot_info.rs | 135 ++++++++++++++++++ 13 files changed, 240 insertions(+), 32 deletions(-) create mode 100644 libs/memory/docs/paging.puml create mode 100644 nucleus/src/boot_info.rs diff --git a/libs/console/Cargo.toml b/libs/console/Cargo.toml index ae6393239..c416e8321 100644 --- a/libs/console/Cargo.toml +++ b/libs/console/Cargo.toml @@ -27,6 +27,7 @@ aarch64-cpu = { workspace = true } liblocking = { workspace = true } liblog = { workspace = true } libqemu = { workspace = true } +libtest = { workspace = true } libtime = { workspace = true } [lib] diff --git a/libs/console/src/write_to.rs b/libs/console/src/write_to.rs index bc14d6165..285b45410 100644 --- a/libs/console/src/write_to.rs +++ b/libs/console/src/write_to.rs @@ -6,6 +6,7 @@ /// No-alloc write!() implementation from /// Requires you to allocate a buffer somewhere manually. // @todo Try to use arrayvec::ArrayString here instead? +// @todo probably use defmt for comms with host? use core::{cmp::min, fmt}; pub struct WriteTo<'a> { diff --git a/libs/driver/Cargo.toml b/libs/driver/Cargo.toml index 0436ebab9..0d2d8de43 100644 --- a/libs/driver/Cargo.toml +++ b/libs/driver/Cargo.toml @@ -25,6 +25,7 @@ maintenance = { status = "experimental" } [dependencies] liblocking = { workspace = true } liblog = { workspace = true } +libtest = { workspace = true } [lib] test = false diff --git a/libs/kernel-state/Cargo.toml b/libs/kernel-state/Cargo.toml index 3610e572d..b21198740 100644 --- a/libs/kernel-state/Cargo.toml +++ b/libs/kernel-state/Cargo.toml @@ -19,6 +19,7 @@ publish = false maintenance = { status = "experimental" } [dependencies] +libtest = { workspace = true } [lints] workspace = true diff --git a/libs/local-irq/Cargo.toml b/libs/local-irq/Cargo.toml index d1e7f8ef5..32f555c11 100644 --- a/libs/local-irq/Cargo.toml +++ b/libs/local-irq/Cargo.toml @@ -21,6 +21,7 @@ maintenance = { status = "experimental" } [dependencies] aarch64-cpu = { workspace = true } liblog = { workspace = true } +libtest = { workspace = true } tock-registers = { workspace = true } [lints] diff --git a/libs/locking/Cargo.toml b/libs/locking/Cargo.toml index 1bbb91af1..d978638af 100644 --- a/libs/locking/Cargo.toml +++ b/libs/locking/Cargo.toml @@ -21,6 +21,7 @@ maintenance = { status = "experimental" } [dependencies] libkernel-state = { workspace = true } liblocal-irq = { workspace = true } +libtest = { workspace = true } [lints] workspace = true diff --git a/libs/log/Cargo.toml b/libs/log/Cargo.toml index 331de2cf8..f6f15f5c1 100644 --- a/libs/log/Cargo.toml +++ b/libs/log/Cargo.toml @@ -19,6 +19,7 @@ publish = false maintenance = { status = "experimental" } [dependencies] +# libtest = { workspace = true } cyclic dep [lints] workspace = true diff --git a/libs/log/src/lib.rs b/libs/log/src/lib.rs index f5dc21d56..12cf21c21 100644 --- a/libs/log/src/lib.rs +++ b/libs/log/src/lib.rs @@ -12,7 +12,7 @@ #![no_main] #![feature(format_args_nl)] #![feature(custom_test_frameworks)] -#![test_runner(libtest::test_runner)] +// #![test_runner(libtest::test_runner)] #![reexport_test_harness_main = "test_main"] use core::{ diff --git a/libs/memory/docs/paging.puml b/libs/memory/docs/paging.puml new file mode 100644 index 000000000..de1ca8e72 --- /dev/null +++ b/libs/memory/docs/paging.puml @@ -0,0 +1,40 @@ +@startuml +'https://plantuml.com/object-diagram + +object GiantPage +object GiantPage_2 +object LargePage +object Page +object Page_2 +object Unmapped + +map L1PageUpperDirectory_2 { + entry0 *--> GiantPage_2 + entry1 *--> Unmapped +} + +map L3PageTable { + entry0 *--> Page + entry1 *--> Page_2 +} + +map L2PageDirectory { + entry0 *-> L3PageTable + entry1 *--> LargePage +} + +map L1PageUpperDirectory { + entry0 *-> L2PageDirectory + entry1 *--> GiantPage +} + +map L0PageGlobalDirectory { + entry0 *-> L1PageUpperDirectory + entry1 *--> L1PageUpperDirectory_2 +} + +map VirtSpace { + root *-> L0PageGlobalDirectory +} + +@enduml diff --git a/libs/memory/src/arch/aarch64/mmu/mod.rs b/libs/memory/src/arch/aarch64/mmu/mod.rs index 101b167e8..1a1ff648b 100644 --- a/libs/memory/src/arch/aarch64/mmu/mod.rs +++ b/libs/memory/src/arch/aarch64/mmu/mod.rs @@ -1,6 +1,8 @@ use { crate::{ - Address, Physical, mmu::{AddressSpace, MMUEnableError, TranslationGranule, interface}, platform::{self, memory::mmu::KERNEL_TABLES} + Address, Physical, + mmu::{AddressSpace, MMUEnableError, TranslationGranule, interface}, + platform::{self, memory::mmu::KERNEL_TABLES}, }, aarch64_cpu::{ asm::{self, barrier}, @@ -197,7 +199,7 @@ impl interface::MMU for MemoryManagementUnit { // use cortex_a::regs::RegisterReadWrite; // Enable the MMU and turn on data and instruction caching. -, + SCTLR_EL1.modify( SCTLR_EL1::EE::LittleEndian // Endianness select in EL1 + SCTLR_EL1::E0E::LittleEndian // Endianness select in EL0 @@ -264,7 +266,7 @@ enum PageTableEntry { /// A page PageTableEntry::descriptor with 4 KiB aperture. /// /// The output points to physical memory. - PageDescriptor(EntryFlags), + PageDescriptor(PageFlags), } /// A descriptor pointing to the next page table. (within PageTableEntry enum) diff --git a/libs/memory/src/arch/aarch64/mmu/translation_table.rs b/libs/memory/src/arch/aarch64/mmu/translation_table.rs index 82d42b52c..db1223778 100644 --- a/libs/memory/src/arch/aarch64/mmu/translation_table.rs +++ b/libs/memory/src/arch/aarch64/mmu/translation_table.rs @@ -1,8 +1,14 @@ +use core::{ + marker::PhantomData, + ops::{Index, IndexMut}, +}; + use { - super::{mair, Granule512MiB, Granule64KiB}, + super::{Granule64KiB, Granule512MiB, mair}, crate::{ + Address, Physical, Virtual, mmu::{AccessPermissions, AttributeFields, MemAttributes, MemoryRegion, PageAddress}, - platform, Address, Physical, Virtual, + platform, }, core::convert, tock_registers::{ @@ -21,7 +27,7 @@ register_bitfields! { /// A table descriptor, as per ARMv8-A Architecture Reference Manual Figure D5-15. /// AArch64 Reference Manual page 2150, D5-2445 - STAGE1_TABLE_DESCRIPTOR [ + pub(crate) STAGE1_TABLE_DESCRIPTOR [ /// Physical address of the next descriptor. NEXT_LEVEL_TABLE_ADDR_64KiB OFFSET(16) NUMBITS(32) [], // [47:16] NEXT_LEVEL_TABLE_ADDR_4KiB OFFSET(12) NUMBITS(36) [], // [47:12] @@ -43,7 +49,7 @@ register_bitfields! { /// A level 3 page descriptor, as per ARMv8-A Architecture Reference Manual Figure D5-17. /// AArch64 Reference Manual page 2150, D5-2445 - STAGE1_PAGE_DESCRIPTOR [ + pub(crate) STAGE1_PAGE_DESCRIPTOR [ /// Unprivileged execute-never. UXN OFFSET(54) NUMBITS(1) [ Execute = 0, @@ -409,7 +415,7 @@ impl FixedSizeTranslationTable { } } -impl memory::mmu::translation_table::interface::TranslationTable +impl crate::mmu::translation_table::interface::TranslationTable for FixedSizeTranslationTable { fn init(&mut self) -> Result<(), &'static str> { @@ -543,7 +549,8 @@ impl PageSize for Size2MiB { impl NotGiantPageSize for Size2MiB {} -type EntryFlags = tock_registers::fields::FieldValue; +type PageFlags = tock_registers::fields::FieldValue; +type TableFlags = tock_registers::fields::FieldValue; // type EntryRegister = register::LocalRegisterCopy; /// L0 table -- only pointers to L1 tables @@ -633,18 +640,18 @@ enum PageTableEntry { /// Table descriptor is a L0, L1 or L2 table pointing to another table. /// L0 tables can only point to L1 tables. /// A descriptor pointing to the next page table. - TableDescriptor(EntryFlags), + TableDescriptor(TableFlags), /// A Level2 block descriptor with 2 MiB aperture. /// /// The output points to physical memory. - Lvl2BlockDescriptor(EntryFlags), + Lvl2BlockDescriptor(TableFlags), /// A page PageTableEntry::descriptor with 4 KiB aperture. /// /// The output points to physical memory. - PageDescriptor(EntryFlags), + PageDescriptor(PageFlags), } -/// A descriptor pointing to the next page table. (within PageTableEntry enum) +// A descriptor pointing to the next page table. (within PageTableEntry enum) // struct TableDescriptor(register::FieldValue); impl PageTableEntry { @@ -657,17 +664,16 @@ impl PageTableEntry { let shifted = next_lvl_table_addr >> Size4KiB::SHIFT; Ok(PageTableEntry::TableDescriptor( - STAGE1_DESCRIPTOR::VALID::True - + STAGE1_DESCRIPTOR::AF::Enabled - + STAGE1_DESCRIPTOR::TYPE::Table - + STAGE1_DESCRIPTOR::NEXT_LVL_TABLE_ADDR_4KiB.val(shifted as u64), + STAGE1_TABLE_DESCRIPTOR::VALID::True + + STAGE1_TABLE_DESCRIPTOR::TYPE::Table + + STAGE1_TABLE_DESCRIPTOR::NEXT_LEVEL_TABLE_ADDR_4KiB.val(shifted as u64), )) } } -/// A Level2 block descriptor with 2 MiB aperture. -/// -/// The output points to physical memory. +// A Level2 block descriptor with 2 MiB aperture. +// +// The output points to physical memory. // struct Lvl2BlockDescriptor(register::FieldValue); impl PageTableEntry { @@ -682,18 +688,17 @@ impl PageTableEntry { let shifted = output_addr >> Size2MiB::SHIFT; Ok(PageTableEntry::Lvl2BlockDescriptor( - STAGE1_DESCRIPTOR::VALID::True - + STAGE1_DESCRIPTOR::AF::Enabled - + STAGE1_DESCRIPTOR::TYPE::Block - + STAGE1_DESCRIPTOR::LVL2_OUTPUT_ADDR_4KiB.val(shifted as u64) + STAGE1_TABLE_DESCRIPTOR::VALID::True + + STAGE1_TABLE_DESCRIPTOR::TYPE::Block + + STAGE1_TABLE_DESCRIPTOR::NEXT_LEVEL_TABLE_ADDR_4KiB.val(shifted as u64) + attribute_fields.into(), )) } } -/// A page descriptor with 4 KiB aperture. -/// -/// The output points to physical memory. +// A page descriptor with 4 KiB aperture. +// +// The output points to physical memory. impl PageTableEntry { fn new_page_descriptor( @@ -707,10 +712,10 @@ impl PageTableEntry { let shifted = output_addr >> Size4KiB::SHIFT; Ok(PageTableEntry::PageDescriptor( - STAGE1_DESCRIPTOR::VALID::True - + STAGE1_DESCRIPTOR::AF::Enabled - + STAGE1_DESCRIPTOR::TYPE::Table - + STAGE1_DESCRIPTOR::NEXT_LVL_TABLE_ADDR_4KiB.val(shifted as u64) + STAGE1_PAGE_DESCRIPTOR::VALID::True + + STAGE1_PAGE_DESCRIPTOR::AF::Enabled + + STAGE1_PAGE_DESCRIPTOR::TYPE::Table + + STAGE1_PAGE_DESCRIPTOR::NEXT_LVL_TABLE_ADDR_4KiB.val(shifted as u64) + attribute_fields.into(), )) } diff --git a/libs/platform/src/platform/raspberrypi/linker/kernel.ld b/libs/platform/src/platform/raspberrypi/linker/kernel.ld index d59505e81..35a4c71cd 100644 --- a/libs/platform/src/platform/raspberrypi/linker/kernel.ld +++ b/libs/platform/src/platform/raspberrypi/linker/kernel.ld @@ -35,6 +35,7 @@ PHDRS SECTIONS { . = __phys_mem_start; + __init_thread_start = .; /*********************************************************************************************** * Boot Core Stack @@ -71,11 +72,19 @@ SECTIONS __BOOT_END = .; /* Here the boot code ends */ ASSERT((__BOOT_END & PAGE_MASK) == 0, "End of boot code is not page aligned") + __init_thread_end = .; + /******************************************************************************************* * Regular Kernel Code *******************************************************************************************/ + /* Physical addresses */ + __nucleus_start = .; __CODE_START = .; + __kernel_virt_addr_space_start = .; + + /* Kernel virtual address */ + . = KERNEL_VIRT_ADDR_BASE; *(.text*) @@ -115,6 +124,10 @@ SECTIONS FILL(0x00) . = ALIGN(PAGE_SIZE); + PHYS_KERNEL_TABLES_BASE_ADDR = .; + KERNEL_TABLES = .; + + . += 0x20000; /* Random for now */ } :segment_data .bss (NOLOAD) : ALIGN(PAGE_SIZE) @@ -126,8 +139,10 @@ SECTIONS . = ALIGN(PAGE_SIZE); /* Align up to page size */ __BSS_END = .; __BSS_SIZE_U64S = (__BSS_END - __BSS_START) / 8; /* TODO: remove */ + } :segment_data + . = ALIGN(PAGE_SIZE); /* Align up to page size */ __DATA_END = .; /*********************************************************************************************** @@ -138,6 +153,10 @@ SECTIONS __MMIO_REMAP_END = .; ASSERT((. & PAGE_MASK) == 0, "MMIO remap reservation is not page aligned") + __kernel_virt_addr_space_size = . - __kernel_virt_addr_space_start; + + /* Physical end */ + __nucleus_end = __nucleus_start + __kernel_virt_addr_space_size; /*********************************************************************************************** * Misc diff --git a/nucleus/src/boot_info.rs b/nucleus/src/boot_info.rs new file mode 100644 index 000000000..cf41af878 --- /dev/null +++ b/nucleus/src/boot_info.rs @@ -0,0 +1,135 @@ +#![allow(dead_code)] + +use crate::{memory::PhysAddr, println, sync}; + +#[derive(Default, Copy, Clone)] +struct BootInfoMemRegion { + pub start: PhysAddr, + pub end: PhysAddr, +} + +impl BootInfoMemRegion { + pub const fn new() -> BootInfoMemRegion { + BootInfoMemRegion { + start: PhysAddr::zero(), + end: PhysAddr::zero(), + } + } + + pub fn size(&self) -> u64 { + self.end - self.start + } + + pub fn is_empty(&self) -> bool { + self.start == self.end + } +} + +const NUM_MEM_REGIONS: usize = 16; + +pub enum BootInfoError { + NoFreeMemRegions, +} + +#[derive(Default)] +struct BootInfo { + pub regions: [BootInfoMemRegion; NUM_MEM_REGIONS], + pub max_slot_pos: usize, +} + +impl BootInfo { + pub const fn new() -> BootInfo { + BootInfo { + regions: [BootInfoMemRegion::new(); NUM_MEM_REGIONS], + max_slot_pos: 0, + } + } + + pub fn insert_region(&mut self, reg: BootInfoMemRegion) -> Result<(), BootInfoError> { + if reg.is_empty() { + return Ok(()); + } + assert!(reg.start <= reg.end); + for region in self.regions.iter_mut() { + if region.is_empty() { + *region = reg; + return Ok(()); + } + } + return Err(BootInfoError::NoFreeMemRegions); + } + + pub fn alloc_region(&mut self, size_bits: usize) -> Result { + let mut reg_index: usize = 0; + let mut reg: BootInfoMemRegion = BootInfoMemRegion::new(); + let mut rem_small: BootInfoMemRegion = BootInfoMemRegion::new(); + let mut rem_large: BootInfoMemRegion = BootInfoMemRegion::new(); + /* + * Search for a free mem region that will be the best fit for an allocation. We favour allocations + * that are aligned to either end of the region. If an allocation must split a region we favour + * an unbalanced split. In both cases we attempt to use the smallest region possible. In general + * this means we aim to make the size of the smallest remaining region smaller (ideally zero) + * followed by making the size of the largest remaining region smaller. + */ + for (i, reg_iter) in self.regions.iter().enumerate() { + let mut new_reg: BootInfoMemRegion = BootInfoMemRegion::new(); + + /* Determine whether placing the region at the start or the end will create a bigger left over region */ + if reg_iter.start.aligned_up(1usize << size_bits) - reg_iter.start + < reg_iter.end - reg_iter.end.aligned_down(1usize << size_bits) + { + new_reg.start = reg_iter.start.aligned_up(1usize << size_bits); + new_reg.end = new_reg.start + (1u64 << size_bits); + } else { + new_reg.end = reg_iter.end.aligned_down(1usize << size_bits); + new_reg.start = new_reg.end - (1u64 << size_bits); + } + if new_reg.end > new_reg.start + && new_reg.start >= reg_iter.start + && new_reg.end <= reg_iter.end + { + let mut new_rem_small: BootInfoMemRegion = BootInfoMemRegion::new(); + let mut new_rem_large: BootInfoMemRegion = BootInfoMemRegion::new(); + + if new_reg.start - reg_iter.start < reg_iter.end - new_reg.end { + new_rem_small.start = reg_iter.start; + new_rem_small.end = new_reg.start; + new_rem_large.start = new_reg.end; + new_rem_large.end = reg_iter.end; + } else { + new_rem_large.start = reg_iter.start; + new_rem_large.end = new_reg.start; + new_rem_small.start = new_reg.end; + new_rem_small.end = reg_iter.end; + } + if reg.is_empty() + || (new_rem_small.size() < rem_small.size()) + || (new_rem_small.size() == rem_small.size() + && new_rem_large.size() < rem_large.size()) + { + reg = new_reg; + rem_small = new_rem_small; + rem_large = new_rem_large; + reg_index = i; + } + } + } + if reg.is_empty() { + panic!("Kernel init failed: not enough memory\n"); + } + /* Remove the region in question */ + self.regions[reg_index] = BootInfoMemRegion::new(); + /* Add the remaining regions in largest to smallest order */ + self.insert_region(rem_large)?; + if self.insert_region(rem_small).is_err() { + println!( + "BootInfo::alloc_region(): wasted {} bytes due to alignment, try to increase NUM_MEM_REGIONS", + rem_small.size() + ); + } + Ok(reg.start) + } +} + +#[link_section = ".data.boot"] // @todo put zero-initialized stuff to .bss.boot! +static BOOT_INFO: sync::NullLock = sync::NullLock::new(BootInfo::new()); From 1404f85bd4b3f6c41489e9f77bf974a025fcf50d Mon Sep 17 00:00:00 2001 From: Berkus Decker Date: Mon, 19 Jan 2026 03:42:51 +0200 Subject: [PATCH 007/107] wip: boot init-thread in EL2 - make compile --- Cargo.toml | 1 + libs/boot/src/arch/aarch64/boot.rs | 111 ++++----- libs/boot/src/arch/aarch64/boot.s | 6 +- libs/memory/Cargo.toml | 1 + libs/memory/src/arch/aarch64/features.rs | 4 +- libs/memory/src/arch/aarch64/mmu/mod.rs | 211 +++++++++--------- .../src/arch/aarch64/mmu/translation_table.rs | 49 ++-- libs/memory/src/arch/aarch64/phys_frame.rs | 6 +- libs/memory/src/arch/aarch64/virt_page.rs | 6 +- libs/memory/src/lib.rs | 2 + libs/memory/src/phys_addr.rs | 10 +- .../src/platform/raspberrypi/memory/mmu.rs | 77 ++++--- libs/memory/src/virt_addr.rs | 12 +- .../src/platform/raspberrypi/linker/kernel.ld | 39 ++-- nucleus/src/main.rs | 58 ++--- targets/aarch64-metta-none-eabi.json | 3 - 16 files changed, 302 insertions(+), 294 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 336fc40cd..296f01be1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -60,6 +60,7 @@ libprimitives = { path = "libs/primitives" } libqemu = { path = "libs/qemu" } libtest = { path = "libs/test" } libtime = { path = "libs/time" } +num = { version = "0.4", default-features = false } once_cell = { version = "1.21", default-features = false, features = ["unstable"] } qemu-exit = { version = "3.0" } snafu = { version = "0.8", default-features = false, features = ["unstable-core-error"] } diff --git a/libs/boot/src/arch/aarch64/boot.rs b/libs/boot/src/arch/aarch64/boot.rs index 670a162d2..ede8420f7 100644 --- a/libs/boot/src/arch/aarch64/boot.rs +++ b/libs/boot/src/arch/aarch64/boot.rs @@ -9,8 +9,8 @@ //! use { - aarch64_cpu::{asm, registers::*}, - core::{arch::global_asm, cell::UnsafeCell}, + aarch64_cpu::registers::*, + core::arch::global_asm, libcpu::endless_sleep, libplatform::platform::cpu::BOOT_CORE_ID, tock_registers::interfaces::{Readable, Writeable}, @@ -52,7 +52,7 @@ global_asm!( /// # Safety /// /// Totally unsafe! We're in the hardware land. -/// We assume that no statics are accessed before transition to main from `reset()` function. +/// We assume that no statics are accessed before transition to this fn. #[unsafe(no_mangle)] #[unsafe(link_section = ".text.boot")] pub unsafe extern "C" fn _startup_in_rust() -> ! { @@ -66,10 +66,10 @@ pub unsafe extern "C" fn _startup_in_rust() -> ! { match CurrentEL.get() { #[cfg(feature = "qemu")] - EL3 => setup_and_enter_el1_from_el3(), - EL2 => setup_and_enter_el1_from_el2(), - EL1 => reset(), // Cannot configure memory mappings here properly, fail instead? - // if not core0 or not EL3/EL2/EL1, infinitely wait for events + EL3 => setup_and_enter_el2_from_el3(), + EL2 => setup_and_enter_el2_from_el2(), + EL1 => reset(), // Cannot configure memory mappings here properly, fail instead! + // if not core0 or not EL3/EL2, infinitely wait for events _ => endless_sleep(), } } @@ -96,6 +96,8 @@ fn shared_setup_and_enter_pre() { + SCTLR_EL1::SA0::Disable, ); + // Same for SCTLR_EL2? + // enable_armv6_unaligned_access(); // Set Hypervisor Configuration Register (EL2) @@ -105,51 +107,53 @@ fn shared_setup_and_enter_pre() { // @todo disable VM bit to prevent stage 2 MMU translations } -#[unsafe(link_section = ".text.boot")] -#[inline] -fn shared_setup_and_enter_post() -> ! { - unsafe extern "Rust" { - // Stack top - static __STACK_TOP: UnsafeCell<()>; - } - // Set up SP_EL1 (stack pointer), which will be used by EL1 once - // we "return" to it. - // SAFETY: Pure asm. - unsafe { - SP_EL1.set(__STACK_TOP.get() as u64); - } - - // Use `eret` to "return" to EL1. This will result in execution of - // `reset()` in EL1. - asm::eret() -} - +// #[unsafe(link_section = ".text.boot")] +// #[inline] +// fn shared_setup_and_enter_post() -> ! { +// unsafe extern "Rust" { +// // Stack top +// static __STACK_TOP: UnsafeCell<()>; +// } +// // Set up SP_EL2 (stack pointer), which will be used by EL2 once +// // we "return" to it. +// // SAFETY: Pure asm. +// unsafe { +// SP_EL2.set(__STACK_TOP.get() as u64); +// } +// // Use `eret` to "return" to EL2. This will result in execution of +// // `reset()` in EL2. +// asm::eret() +// } + +// FIXME: This will be called by init_thread later. /// Real hardware boot-up sequence. /// /// Prepare and execute transition from EL2 to EL1. +// #[unsafe(link_section = ".text.boot")] +// #[inline] +// fn setup_and_enter_el1_from_el2() -> ! { +// // Set Saved Program Status Register (EL2) +// // Set up a simulated exception return. +// // +// // Fake a saved program status, where all interrupts were +// // masked and SP_EL1 was used as a stack pointer. +// SPSR_EL2.write( +// SPSR_EL2::D::Masked +// + SPSR_EL2::A::Masked +// + SPSR_EL2::I::Masked +// + SPSR_EL2::F::Masked +// + SPSR_EL2::M::EL1h, // Use SP_EL1 +// ); +// // Make the Exception Link Register (EL2) point to reset(). +// #[allow(clippy::fn_to_numeric_cast_any)] +// ELR_EL2.set(reset as *const () as u64); +// shared_setup_and_enter_post() +// } + #[unsafe(link_section = ".text.boot")] #[inline] -fn setup_and_enter_el1_from_el2() -> ! { - // Set Saved Program Status Register (EL2) - // Set up a simulated exception return. - // - // Fake a saved program status, where all interrupts were - // masked and SP_EL1 was used as a stack pointer. - SPSR_EL2.write( - SPSR_EL2::D::Masked - + SPSR_EL2::A::Masked - + SPSR_EL2::I::Masked - + SPSR_EL2::F::Masked - + SPSR_EL2::M::EL1h, // Use SP_EL1 - ); - - // Set up EL1 mmu tables from precomputed KERNEL_TABLES. - - // Make the Exception Link Register (EL2) point to reset(). - #[allow(clippy::fn_to_numeric_cast_any)] - ELR_EL2.set(reset as *const () as u64); - - shared_setup_and_enter_post() +fn setup_and_enter_el2_from_el2() -> ! { + reset(); } /// QEMU boot-up sequence. @@ -160,12 +164,12 @@ fn setup_and_enter_el1_from_el2() -> ! { /// However, GPU init code must be switching it down to EL2. /// QEMU can't emulate Raspberry Pi properly (no VC boot code), so it starts in EL3. /// -/// Prepare and execute transition from EL3 to EL1. +/// Prepare and execute transition from EL3 to EL2. /// (from https://github.com/s-matyukevich/raspberry-pi-os/blob/master/docs/lesson02/rpi-os.md) #[cfg(feature = "qemu")] #[unsafe(link_section = ".text.boot")] #[inline] -fn setup_and_enter_el1_from_el3() -> ! { +fn setup_and_enter_el2_from_el3() -> ! { // Set Secure Configuration Register (EL3) SCR_EL3.write(SCR_EL3::RW::NextELIsAarch64 + SCR_EL3::NS::NonSecure); @@ -173,24 +177,23 @@ fn setup_and_enter_el1_from_el3() -> ! { // Set up a simulated exception return. // // Fake a saved program status, where all interrupts were - // masked and SP_EL1 was used as a stack pointer. + // masked and SP_EL2 was used as a stack pointer. SPSR_EL3.write( SPSR_EL3::D::Masked + SPSR_EL3::A::Masked + SPSR_EL3::I::Masked + SPSR_EL3::F::Masked - + SPSR_EL3::M::EL1h, // Use SP_EL1 + + SPSR_EL3::M::EL2h, // Use SP_EL2 ); - // Set up EL1 mmu tables from precomputed KERNEL_TABLES. - // MMU::enable_mmu_and_caching(); - // Make the Exception Link Register (EL3) point to reset(). ELR_EL3.set(reset as *const () as u64); shared_setup_and_enter_post() } +// Enter Rust code in EL2. +#[unsafe(link_section = ".text.boot")] fn reset() -> ! { unsafe extern "Rust" { fn main() -> !; diff --git a/libs/boot/src/arch/aarch64/boot.s b/libs/boot/src/arch/aarch64/boot.s index 3e65f874b..e8cb4f8ec 100644 --- a/libs/boot/src/arch/aarch64/boot.s +++ b/libs/boot/src/arch/aarch64/boot.s @@ -70,9 +70,9 @@ _boot_cores: // Initialize BSS - prepare to fearlessly call into Rust code. // Assumptions: BSS start is u128-aligned, BSS end is u128-aligned. - // __BSS_START and __BSS_END are defined in linker script - ADR_REL x1, __BSS_START - ADR_REL x2, __BSS_END + // __BSS_START and __BSS_END are defined in the linker script + ADR_REL x1, __BSS_START // must be physical address!!!1 + ADR_REL x2, __BSS_END // must be physical address!!!1 .L__bss_init_loop: cmp x1, x2 b.eq .L_setup_stack diff --git a/libs/memory/Cargo.toml b/libs/memory/Cargo.toml index 616a246e6..f3c6e062a 100644 --- a/libs/memory/Cargo.toml +++ b/libs/memory/Cargo.toml @@ -26,6 +26,7 @@ buddy-alloc = { workspace = true } cfg-if = { workspace = true } liblocking = { workspace = true } liblog = { workspace = true } +num = { workspace = true } once_cell = { workspace = true } snafu = { workspace = true } tock-registers = { workspace = true } diff --git a/libs/memory/src/arch/aarch64/features.rs b/libs/memory/src/arch/aarch64/features.rs index dfc45ecef..9d442f267 100644 --- a/libs/memory/src/arch/aarch64/features.rs +++ b/libs/memory/src/arch/aarch64/features.rs @@ -1,8 +1,8 @@ //! Print MMU suported features for debugging. use { - crate::println, - cortex_a::registers::{ID_AA64MMFR0_EL1, SCTLR_EL1, TCR_EL1}, + aarch64_cpu::registers::{ID_AA64MMFR0_EL1, SCTLR_EL1, TCR_EL1}, + liblog::println, tock_registers::interfaces::Readable, }; diff --git a/libs/memory/src/arch/aarch64/mmu/mod.rs b/libs/memory/src/arch/aarch64/mmu/mod.rs index 1a1ff648b..529f6fb57 100644 --- a/libs/memory/src/arch/aarch64/mmu/mod.rs +++ b/libs/memory/src/arch/aarch64/mmu/mod.rs @@ -1,14 +1,18 @@ use { crate::{ Address, Physical, - mmu::{AddressSpace, MMUEnableError, TranslationGranule, interface}, - platform::{self, memory::mmu::KERNEL_TABLES}, + arch::mmu::translation_table::{ + PageFlags, PageSize, STAGE1_PAGE_DESCRIPTOR, STAGE1_TABLE_DESCRIPTOR, Size2MiB, + Size4KiB, TableFlags, + }, + mmu::{AddressSpace, AttributeFields, MMUEnableError, TranslationGranule, interface}, }, aarch64_cpu::{ asm::{self, barrier}, - registers::{ID_AA64MMFR0_EL1, SCTLR_EL1, TCR_EL1, TTBR0_EL1, TTBR1_EL1}, + registers::{ID_AA64MMFR0_EL1, SCTLR_EL1, TCR_EL1}, }, core::intrinsics::unlikely, + liblog::println, tock_registers::interfaces::{ReadWriteable, Readable, Writeable}, }; @@ -138,7 +142,7 @@ pub fn mmu() -> &'static impl interface::MMU { impl interface::MMU for MemoryManagementUnit { unsafe fn enable_mmu_and_caching( &self, - phys_tables_base_addr: Address, + _phys_tables_base_addr: Address, ) -> Result<(), MMUEnableError> { if unlikely(self.is_enabled()) { return Err(MMUEnableError::AlreadyEnabled); @@ -173,8 +177,8 @@ impl interface::MMU for MemoryManagementUnit { // Point to the LVL2 table base address in TTBR0. // TODO: USER_TABLES, not KERNEL_TABLES here? - TTBR0_EL1.set_baddr(KERNEL_TABLES.entries.base_addr_u64()); // User (lo-)space addresses - TTBR0_EL1.modify(TTBR0_EL1::CnP.val(1)); + // TTBR0_EL1.set_baddr(KERNEL_TABLES.entries.base_addr_u64()); // User (lo-)space addresses + // TTBR0_EL1.modify(TTBR0_EL1::CnP.val(1)); // TODO: also do kernel level tables (same mappings but at higher table addresses? need to update ttt to do it) // TTBR1_EL1.set_baddr(LVL1_TABLE.entries.base_addr_u64()); // Kernel (hi-)space addresses @@ -247,6 +251,10 @@ impl interface::MMU for MemoryManagementUnit { fn is_enabled(&self) -> bool { SCTLR_EL1.matches_all(SCTLR_EL1::M::Enable) } + + fn print_features(&self) { + todo!() + } } /// Type-safe enum wrapper covering Table's 64-bit entries. @@ -258,18 +266,18 @@ enum PageTableEntry { /// Table descriptor is a L0, L1 or L2 table pointing to another table. /// L0 tables can only point to L1 tables. /// A descriptor pointing to the next page table. - TableDescriptor(EntryFlags), + TableDescriptor(TableFlags), /// A Level2 block descriptor with 2 MiB aperture. /// /// The output points to physical memory. - Lvl2BlockDescriptor(EntryFlags), + Lvl2BlockDescriptor(TableFlags), /// A page PageTableEntry::descriptor with 4 KiB aperture. /// /// The output points to physical memory. PageDescriptor(PageFlags), } -/// A descriptor pointing to the next page table. (within PageTableEntry enum) +// A descriptor pointing to the next page table. (within PageTableEntry enum) // struct TableDescriptor(register::FieldValue); impl PageTableEntry { @@ -282,29 +290,29 @@ impl PageTableEntry { let shifted = next_lvl_table_addr >> Size4KiB::SHIFT; Ok(PageTableEntry::TableDescriptor( - STAGE1_DESCRIPTOR::VALID::True - + STAGE1_DESCRIPTOR::TYPE::Table - + STAGE1_DESCRIPTOR::NEXT_LVL_TABLE_ADDR_4KiB.val(shifted as u64), + STAGE1_TABLE_DESCRIPTOR::VALID::True + + STAGE1_TABLE_DESCRIPTOR::TYPE::Table + + STAGE1_TABLE_DESCRIPTOR::NEXT_LEVEL_TABLE_ADDR_4KiB.val(shifted as u64), )) } } -#[derive(Snafu, Debug)] +#[derive(Debug)] //Snafu, enum PageTableError { - #[snafu(display("BlockDescriptor: Address is not 2 MiB aligned."))] + // #[snafu(display("BlockDescriptor: Address is not 2 MiB aligned."))] //"PageDescriptor: Address is not 4 KiB aligned." NotAligned(&'static str), } -/// A Level2 block descriptor with 2 MiB aperture. -/// -/// The output points to physical memory. +// A Level2 block descriptor with 2 MiB aperture. +// +// The output points to physical memory. // struct Lvl2BlockDescriptor(register::FieldValue); impl PageTableEntry { fn new_lvl2_block_descriptor( output_addr: usize, - attribute_fields: AttributeFields, + _attribute_fields: AttributeFields, ) -> Result { if output_addr % Size2MiB::SIZE as usize != 0 { return Err(PageTableError::NotAligned(Size2MiB::SIZE_AS_DEBUG_STR)); @@ -313,23 +321,23 @@ impl PageTableEntry { let shifted = output_addr >> Size2MiB::SHIFT; Ok(PageTableEntry::Lvl2BlockDescriptor( - STAGE1_DESCRIPTOR::VALID::True - + STAGE1_DESCRIPTOR::AF::True - + into_mmu_attributes(attribute_fields) - + STAGE1_DESCRIPTOR::TYPE::Block - + STAGE1_DESCRIPTOR::LVL2_OUTPUT_ADDR_4KiB.val(shifted as u64), + STAGE1_TABLE_DESCRIPTOR::VALID::True + // + STAGE1_TABLE_DESCRIPTOR::AF::Accessed + // + into_mmu_attributes(attribute_fields) + + STAGE1_TABLE_DESCRIPTOR::TYPE::Block + + STAGE1_TABLE_DESCRIPTOR::NEXT_LEVEL_TABLE_ADDR_4KiB.val(shifted as u64), )) } } -/// A page descriptor with 4 KiB aperture. -/// -/// The output points to physical memory. +// A page descriptor with 4 KiB aperture. +// +// The output points to physical memory. impl PageTableEntry { fn new_page_descriptor( output_addr: usize, - attribute_fields: AttributeFields, + _attribute_fields: AttributeFields, ) -> Result { if output_addr % Size4KiB::SIZE as usize != 0 { return Err(PageTableError::NotAligned(Size4KiB::SIZE_AS_DEBUG_STR)); @@ -338,11 +346,11 @@ impl PageTableEntry { let shifted = output_addr >> Size4KiB::SHIFT; Ok(PageTableEntry::PageDescriptor( - STAGE1_DESCRIPTOR::VALID::True - + STAGE1_DESCRIPTOR::AF::True - + into_mmu_attributes(attribute_fields) - + STAGE1_DESCRIPTOR::TYPE::Table - + STAGE1_DESCRIPTOR::NEXT_LVL_TABLE_ADDR_4KiB.val(shifted as u64), + STAGE1_PAGE_DESCRIPTOR::VALID::True + // + STAGE1_TABLE_DESCRIPTOR::AF::Accessed + // + into_mmu_attributes(attribute_fields) + + STAGE1_PAGE_DESCRIPTOR::TYPE::Page + + STAGE1_PAGE_DESCRIPTOR::OUTPUT_ADDR_4KiB.val(shifted as u64), )) } } @@ -361,9 +369,8 @@ impl From for u64 { fn from(val: PageTableEntry) -> u64 { match val { PageTableEntry::Invalid => 0, - PageTableEntry::TableDescriptor(x) - | PageTableEntry::Lvl2BlockDescriptor(x) - | PageTableEntry::PageDescriptor(x) => x.value, + PageTableEntry::TableDescriptor(x) | PageTableEntry::Lvl2BlockDescriptor(x) => x.value, + PageTableEntry::PageDescriptor(x) => x.value, } } } @@ -408,53 +415,53 @@ impl BaseAddr for [u64; 512] { /// restart the CPU. pub unsafe fn init() -> Result<(), &'static str> { // Prepare the memory attribute indirection register. - mair::set_up(); + // mair::set_up(); // Point the first 2 MiB of virtual addresses to the follow-up LVL3 // page-table. - LVL2_TABLE.entries[0] = - PageTableEntry::new_table_descriptor(LVL3_TABLE.entries.base_addr_usize())?.into(); + // LVL2_TABLE.entries[0] = + // PageTableEntry::new_table_descriptor(LVL3_TABLE.entries.base_addr_usize())?.into(); // Fill the rest of the LVL2 (2 MiB) entries as block descriptors. // // Notice the skip(1) which makes the iteration start at the second 2 MiB // block (0x20_0000). - for (block_descriptor_nr, entry) in LVL2_TABLE.entries.iter_mut().enumerate().skip(1) { - let virt_addr = block_descriptor_nr << Size2MiB::SHIFT; + // for (block_descriptor_nr, entry) in LVL2_TABLE.entries.iter_mut().enumerate().skip(1) { + // let virt_addr = block_descriptor_nr << Size2MiB::SHIFT; - let (output_addr, attribute_fields) = match get_virt_addr_properties(virt_addr) { - Err(s) => return Err(s), - Ok((a, b)) => (a, b), - }; + // let (output_addr, attribute_fields) = match get_virt_addr_properties(virt_addr) { + // Err(s) => return Err(s), + // Ok((a, b)) => (a, b), + // }; - let block_desc = - match PageTableEntry::new_lvl2_block_descriptor(output_addr, attribute_fields) { - Err(s) => return Err(s), - Ok(desc) => desc, - }; + // let block_desc = + // match PageTableEntry::new_lvl2_block_descriptor(output_addr, attribute_fields) { + // Err(s) => return Err(s), + // Ok(desc) => desc, + // }; - *entry = block_desc.into(); - } + // *entry = block_desc.into(); + // } // Finally, fill the single LVL3 table (4 KiB granule). - for (page_descriptor_nr, entry) in LVL3_TABLE.entries.iter_mut().enumerate() { - let virt_addr = page_descriptor_nr << Size4KiB::SHIFT; + // for (page_descriptor_nr, entry) in LVL3_TABLE.entries.iter_mut().enumerate() { + // let virt_addr = page_descriptor_nr << Size4KiB::SHIFT; - let (output_addr, attribute_fields) = match get_virt_addr_properties(virt_addr) { - Err(s) => return Err(s), - Ok((a, b)) => (a, b), - }; + // let (output_addr, attribute_fields) = match get_virt_addr_properties(virt_addr) { + // Err(s) => return Err(s), + // Ok((a, b)) => (a, b), + // }; - let page_desc = match PageTableEntry::new_page_descriptor(output_addr, attribute_fields) { - Err(s) => return Err(s), - Ok(desc) => desc, - }; + // let page_desc = match PageTableEntry::new_page_descriptor(output_addr, attribute_fields) { + // Err(s) => return Err(s), + // Ok(desc) => desc, + // }; - *entry = page_desc.into(); - } + // *entry = page_desc.into(); + // } // Point to the LVL2 table base address in TTBR0. - TTBR0_EL1.set_baddr(LVL2_TABLE.entries.base_addr_u64()); // User (lo-)space addresses + // TTBR0_EL1.set_baddr(LVL2_TABLE.entries.base_addr_u64()); // User (lo-)space addresses // TTBR1_EL1.set_baddr(LVL2_TABLE.entries.base_addr_u64()); // Kernel (hi-)space addresses @@ -499,41 +506,41 @@ pub unsafe fn init() -> Result<(), &'static str> { Ok(()) } -/// A function that maps the generic memory range attributes to HW-specific -/// attributes of the MMU. -fn into_mmu_attributes( - attribute_fields: AttributeFields, -) -> FieldValue { - use super::{AccessPermissions, MemAttributes}; - - // Memory attributes - let mut desc = match attribute_fields.mem_attributes { - MemAttributes::CacheableDRAM => { - STAGE1_DESCRIPTOR::SH::InnerShareable - + STAGE1_DESCRIPTOR::AttrIndx.val(mair::attr::NORMAL) - } - MemAttributes::NonCacheableDRAM => { - STAGE1_DESCRIPTOR::SH::InnerShareable - + STAGE1_DESCRIPTOR::AttrIndx.val(mair::attr::NORMAL_NON_CACHEABLE) - } - MemAttributes::Device => { - STAGE1_DESCRIPTOR::SH::OuterShareable - + STAGE1_DESCRIPTOR::AttrIndx.val(mair::attr::DEVICE_NGNRE) - } - }; - - // Access Permissions - desc += match attribute_fields.acc_perms { - AccessPermissions::ReadOnly => STAGE1_DESCRIPTOR::AP::RO_EL1, - AccessPermissions::ReadWrite => STAGE1_DESCRIPTOR::AP::RW_EL1, - }; - - // Execute Never - desc += if attribute_fields.execute_never { - STAGE1_DESCRIPTOR::PXN::NeverExecute - } else { - STAGE1_DESCRIPTOR::PXN::Execute - }; - - desc -} +// A function that maps the generic memory range attributes to HW-specific +// attributes of the MMU. +// fn into_mmu_attributes( +// attribute_fields: AttributeFields, +// ) -> FieldValue { +// use super::{AccessPermissions, MemAttributes}; + +// // Memory attributes +// let mut desc = match attribute_fields.mem_attributes { +// MemAttributes::CacheableDRAM => { +// STAGE1_DESCRIPTOR::SH::InnerShareable +// + STAGE1_DESCRIPTOR::AttrIndx.val(mair::attr::NORMAL) +// } +// MemAttributes::NonCacheableDRAM => { +// STAGE1_DESCRIPTOR::SH::InnerShareable +// + STAGE1_DESCRIPTOR::AttrIndx.val(mair::attr::NORMAL_NON_CACHEABLE) +// } +// MemAttributes::Device => { +// STAGE1_DESCRIPTOR::SH::OuterShareable +// + STAGE1_DESCRIPTOR::AttrIndx.val(mair::attr::DEVICE_NGNRE) +// } +// }; + +// // Access Permissions +// desc += match attribute_fields.acc_perms { +// AccessPermissions::ReadOnly => STAGE1_DESCRIPTOR::AP::RO_EL1, +// AccessPermissions::ReadWrite => STAGE1_DESCRIPTOR::AP::RW_EL1, +// }; + +// // Execute Never +// desc += if attribute_fields.execute_never { +// STAGE1_DESCRIPTOR::PXN::NeverExecute +// } else { +// STAGE1_DESCRIPTOR::PXN::Execute +// }; + +// desc +// } diff --git a/libs/memory/src/arch/aarch64/mmu/translation_table.rs b/libs/memory/src/arch/aarch64/mmu/translation_table.rs index db1223778..326095045 100644 --- a/libs/memory/src/arch/aarch64/mmu/translation_table.rs +++ b/libs/memory/src/arch/aarch64/mmu/translation_table.rs @@ -27,7 +27,7 @@ register_bitfields! { /// A table descriptor, as per ARMv8-A Architecture Reference Manual Figure D5-15. /// AArch64 Reference Manual page 2150, D5-2445 - pub(crate) STAGE1_TABLE_DESCRIPTOR [ + pub STAGE1_TABLE_DESCRIPTOR [ /// Physical address of the next descriptor. NEXT_LEVEL_TABLE_ADDR_64KiB OFFSET(16) NUMBITS(32) [], // [47:16] NEXT_LEVEL_TABLE_ADDR_4KiB OFFSET(12) NUMBITS(36) [], // [47:12] @@ -49,7 +49,7 @@ register_bitfields! { /// A level 3 page descriptor, as per ARMv8-A Architecture Reference Manual Figure D5-17. /// AArch64 Reference Manual page 2150, D5-2445 - pub(crate) STAGE1_PAGE_DESCRIPTOR [ + pub STAGE1_PAGE_DESCRIPTOR [ /// Unprivileged execute-never. UXN OFFSET(54) NUMBITS(1) [ Execute = 0, @@ -396,20 +396,20 @@ impl FixedSizeTranslationTable { // *entry = page_desc.into(); // } - for (l2_nr, l2_entry) in self.lvl2.iter_mut().enumerate() { - *l2_entry = - TableDescriptor::from_next_lvl_table_addr(self.lvl3[l2_nr].base_addr_usize()); + // for (l2_nr, l2_entry) in self.lvl2.iter_mut().enumerate() { + // *l2_entry = + // TableDescriptor::from_next_lvl_table_addr(self.lvl3[l2_nr].base_addr_usize()); - for (l3_nr, l3_entry) in self.lvl3[l2_nr].iter_mut().enumerate() { - let virt_addr = (l2_nr << Granule512MiB::SHIFT) + (l3_nr << Granule64KiB::SHIFT); + // for (l3_nr, l3_entry) in self.lvl3[l2_nr].iter_mut().enumerate() { + // let virt_addr = (l2_nr << Granule512MiB::SHIFT) + (l3_nr << Granule64KiB::SHIFT); - let (phys_output_addr, attribute_fields) = - platform::memory::mmu::unused::virt_mem_layout() - .virt_addr_properties(virt_addr)?; + // let (phys_output_addr, attribute_fields) = + // platform::memory::mmu::unused::virt_mem_layout() + // .virt_addr_properties(virt_addr)?; - *l3_entry = PageDescriptor::from_output_addr(phys_output_addr, &attribute_fields); - } - } + // *l3_entry = PageDescriptor::from_output_addr(phys_output_addr, &attribute_fields); + // } + // } Ok(()) } @@ -549,8 +549,8 @@ impl PageSize for Size2MiB { impl NotGiantPageSize for Size2MiB {} -type PageFlags = tock_registers::fields::FieldValue; -type TableFlags = tock_registers::fields::FieldValue; +pub type PageFlags = tock_registers::fields::FieldValue; +pub type TableFlags = tock_registers::fields::FieldValue; // type EntryRegister = register::LocalRegisterCopy; /// L0 table -- only pointers to L1 tables @@ -679,7 +679,7 @@ impl PageTableEntry { impl PageTableEntry { fn new_lvl2_block_descriptor( output_addr: usize, - attribute_fields: AttributeFields, + _attribute_fields: AttributeFields, ) -> Result { if output_addr % Size2MiB::SIZE as usize != 0 { return Err("BlockDescriptor: Address is not 2 MiB aligned."); @@ -690,8 +690,7 @@ impl PageTableEntry { Ok(PageTableEntry::Lvl2BlockDescriptor( STAGE1_TABLE_DESCRIPTOR::VALID::True + STAGE1_TABLE_DESCRIPTOR::TYPE::Block - + STAGE1_TABLE_DESCRIPTOR::NEXT_LEVEL_TABLE_ADDR_4KiB.val(shifted as u64) - + attribute_fields.into(), + + STAGE1_TABLE_DESCRIPTOR::NEXT_LEVEL_TABLE_ADDR_4KiB.val(shifted as u64), // + attribute_fields.into(), )) } } @@ -703,7 +702,7 @@ impl PageTableEntry { impl PageTableEntry { fn new_page_descriptor( output_addr: usize, - attribute_fields: AttributeFields, + _attribute_fields: AttributeFields, ) -> Result { if output_addr % Size4KiB::SIZE as usize != 0 { return Err("PageDescriptor: Address is not 4 KiB aligned."); @@ -713,10 +712,9 @@ impl PageTableEntry { Ok(PageTableEntry::PageDescriptor( STAGE1_PAGE_DESCRIPTOR::VALID::True - + STAGE1_PAGE_DESCRIPTOR::AF::Enabled - + STAGE1_PAGE_DESCRIPTOR::TYPE::Table - + STAGE1_PAGE_DESCRIPTOR::NEXT_LVL_TABLE_ADDR_4KiB.val(shifted as u64) - + attribute_fields.into(), + // + STAGE1_PAGE_DESCRIPTOR::AF::Accessed + + STAGE1_PAGE_DESCRIPTOR::TYPE::Page + + STAGE1_PAGE_DESCRIPTOR::OUTPUT_ADDR_4KiB.val(shifted as u64), // + attribute_fields.into(), )) } } @@ -735,9 +733,8 @@ impl From for u64 { fn from(val: PageTableEntry) -> u64 { match val { PageTableEntry::Invalid => 0, - PageTableEntry::TableDescriptor(x) - | PageTableEntry::Lvl2BlockDescriptor(x) - | PageTableEntry::PageDescriptor(x) => x.value, + PageTableEntry::TableDescriptor(x) | PageTableEntry::Lvl2BlockDescriptor(x) => x.value, + PageTableEntry::PageDescriptor(x) => x.value, } } } diff --git a/libs/memory/src/arch/aarch64/phys_frame.rs b/libs/memory/src/arch/aarch64/phys_frame.rs index 8737448f8..718d3a6e3 100644 --- a/libs/memory/src/arch/aarch64/phys_frame.rs +++ b/libs/memory/src/arch/aarch64/phys_frame.rs @@ -2,10 +2,8 @@ // Abstractions for default-sized and huge physical memory frames. use { - crate::memory::{ - PhysAddr, - page_size::{PageSize, Size4KiB}, - }, + super::page_size::{PageSize, Size4KiB}, + crate::phys_addr::PhysAddr, core::{ fmt, marker::PhantomData, diff --git a/libs/memory/src/arch/aarch64/virt_page.rs b/libs/memory/src/arch/aarch64/virt_page.rs index 92e7ffff7..86992edb6 100644 --- a/libs/memory/src/arch/aarch64/virt_page.rs +++ b/libs/memory/src/arch/aarch64/virt_page.rs @@ -7,9 +7,9 @@ #![allow(dead_code)] use { - crate::memory::{ - VirtAddr, - page_size::{NotGiantPageSize, PageSize, Size1GiB, Size2MiB, Size4KiB}, + crate::{ + arch::aarch64::page_size::{NotGiantPageSize, PageSize, Size1GiB, Size2MiB, Size4KiB}, + virt_addr::VirtAddr, }, core::{ fmt, diff --git a/libs/memory/src/lib.rs b/libs/memory/src/lib.rs index cd3647e19..69a1056d3 100644 --- a/libs/memory/src/lib.rs +++ b/libs/memory/src/lib.rs @@ -152,3 +152,5 @@ impl fmt::Display for Address { write!(f, "{q1:04x}") } } + +pub const PHYSICAL_KERNEL_WINDOW: u64 = 0xffff_ffff_0000_0000; diff --git a/libs/memory/src/phys_addr.rs b/libs/memory/src/phys_addr.rs index 2ef9bd1df..aa294fd4c 100644 --- a/libs/memory/src/phys_addr.rs +++ b/libs/memory/src/phys_addr.rs @@ -5,8 +5,8 @@ use { crate::{ - memory::VirtAddr, mm::{align_down, align_up}, + virt_addr::VirtAddr, }, bit_field::BitField, core::{ @@ -102,11 +102,11 @@ impl PhysAddr { self.aligned_down(align) == self } - /// Convert physical memory address into a kernel virtual address. + /// Convert physical memory address into a kernel-view virtual address for physical memory. pub fn user_to_kernel(&self) -> VirtAddr { - use super::PHYSICAL_MEMORY_OFFSET; - assert!(self.0 < !PHYSICAL_MEMORY_OFFSET); // Can't have phys address over 1GiB then - VirtAddr::new(self.0 + PHYSICAL_MEMORY_OFFSET) + use super::PHYSICAL_KERNEL_WINDOW; + assert!(self.0 < !PHYSICAL_KERNEL_WINDOW); // Can't have phys address over 1GiB then + VirtAddr::new(self.0 + PHYSICAL_KERNEL_WINDOW) } } diff --git a/libs/memory/src/platform/raspberrypi/memory/mmu.rs b/libs/memory/src/platform/raspberrypi/memory/mmu.rs index 5437e816d..78dbb3175 100644 --- a/libs/memory/src/platform/raspberrypi/memory/mmu.rs +++ b/libs/memory/src/platform/raspberrypi/memory/mmu.rs @@ -2,11 +2,10 @@ use { crate::{ + Physical, Virtual, mmu::{ - self as generic_mmu, AccessPermissions, AddressSpace, AssociatedTranslationTable, - AttributeFields, MemAttributes, MemoryRegion, PageAddress, TranslationGranule, + AddressSpace, AssociatedTranslationTable, MemoryRegion, PageAddress, TranslationGranule, }, - Physical, Virtual, }, liblocking::InitStateLock, }; @@ -193,18 +192,18 @@ pub fn virt_mmio_remap_region() -> MemoryRegion { /// - Any miscalculation or attribute error will likely be fatal. Needs careful manual checking. pub unsafe fn kernel_map_binary() -> Result<(), &'static str> { // SAFETY: Make a mistake and you're dead, gaijin! - unsafe { - generic_mmu::kernel_map_at( - "Kernel boot-core stack", - &virt_boot_core_stack_region(), - &kernel_virt_to_phys_region(virt_boot_core_stack_region()), - AttributeFields { - mem_attributes: MemAttributes::CacheableDRAM, - acc_perms: AccessPermissions::ReadWrite, - execute_never: true, - }, - )?; - } + // unsafe { + // generic_mmu::kernel_map_at( + // "Kernel boot-core stack", + // &virt_boot_core_stack_region(), + // &kernel_virt_to_phys_region(virt_boot_core_stack_region()), + // AttributeFields { + // mem_attributes: MemAttributes::CacheableDRAM, + // acc_perms: AccessPermissions::ReadWrite, + // execute_never: true, + // }, + // )?; + // } // TranslationDescriptor { // name: "Boot code and data", @@ -229,32 +228,32 @@ pub unsafe fn kernel_map_binary() -> Result<(), &'static str> { // }, // SAFETY: Make a mistake and you're dead, gaijin! - unsafe { - generic_mmu::kernel_map_at( - "Kernel code and RO data", - &virt_code_region(), - &kernel_virt_to_phys_region(virt_code_region()), - AttributeFields { - mem_attributes: MemAttributes::CacheableDRAM, - acc_perms: AccessPermissions::ReadOnly, - execute_never: false, - }, - )?; - } + // unsafe { + // generic_mmu::kernel_map_at( + // "Kernel code and RO data", + // &virt_code_region(), + // &kernel_virt_to_phys_region(virt_code_region()), + // AttributeFields { + // mem_attributes: MemAttributes::CacheableDRAM, + // acc_perms: AccessPermissions::ReadOnly, + // execute_never: false, + // }, + // )?; + // } // SAFETY: Make a mistake and you're dead, gaijin! - unsafe { - generic_mmu::kernel_map_at( - "Kernel data and bss", - &virt_data_region(), - &kernel_virt_to_phys_region(virt_data_region()), - AttributeFields { - mem_attributes: MemAttributes::CacheableDRAM, - acc_perms: AccessPermissions::ReadWrite, - execute_never: true, - }, - )?; - } + // unsafe { + // generic_mmu::kernel_map_at( + // "Kernel data and bss", + // &virt_data_region(), + // &kernel_virt_to_phys_region(virt_data_region()), + // AttributeFields { + // mem_attributes: MemAttributes::CacheableDRAM, + // acc_perms: AccessPermissions::ReadWrite, + // execute_never: true, + // }, + // )?; + // } Ok(()) } diff --git a/libs/memory/src/virt_addr.rs b/libs/memory/src/virt_addr.rs index 938ed3910..29b863c84 100644 --- a/libs/memory/src/virt_addr.rs +++ b/libs/memory/src/virt_addr.rs @@ -5,8 +5,8 @@ use { crate::{ - memory::PhysAddr, mm::{align_down, align_up}, + phys_addr::PhysAddr, }, bit_field::BitField, core::{ @@ -14,7 +14,7 @@ use { fmt, ops::{Add, AddAssign, Rem, RemAssign, Sub, SubAssign}, }, - usize_conversions::{usize_from, FromUsize}, + usize_conversions::{FromUsize, usize_from}, ux::*, }; @@ -163,11 +163,11 @@ impl VirtAddr { u9::new(((self.0 >> 12 >> 9 >> 9 >> 9) & 0o777).try_into().unwrap()) } - /// Convert kernel-space virtual address into a physical memory address. + /// Convert kernel-view virtual address of physical memory into a physical memory address. pub fn kernel_to_user(&self) -> PhysAddr { - use super::PHYSICAL_MEMORY_OFFSET; - assert!(self.0 > PHYSICAL_MEMORY_OFFSET); - PhysAddr::new(self.0 - PHYSICAL_MEMORY_OFFSET) + use super::PHYSICAL_KERNEL_WINDOW; + assert!(self.0 > PHYSICAL_KERNEL_WINDOW); + PhysAddr::new(self.0 - PHYSICAL_KERNEL_WINDOW) } } diff --git a/libs/platform/src/platform/raspberrypi/linker/kernel.ld b/libs/platform/src/platform/raspberrypi/linker/kernel.ld index 35a4c71cd..1a7d20bd5 100644 --- a/libs/platform/src/platform/raspberrypi/linker/kernel.ld +++ b/libs/platform/src/platform/raspberrypi/linker/kernel.ld @@ -9,6 +9,7 @@ PAGE_SIZE = 64K; PAGE_MASK = PAGE_SIZE - 1; __phys_mem_start = 0x0; +KERNEL_VIRT_ADDR_BASE = 0xffff000000000000; /* Nucleus is mapped here */ __phys_load_addr = 0x80000; ENTRY(__phys_load_addr); @@ -28,9 +29,10 @@ PHDRS segment_data PT_LOAD FLAGS(6); } -/* Symbols between __BOOT_START and __BOOT_END should be dropped after init is complete. +/* Symbols between __init_thread_start and __init_thread_end should be dropped after init is complete. Symbols between __CODE_START and __CODE_END are the kernel code. Symbols between __BSS_START and __BSS_END must be initialized to zero by startup code in the kernel. + TODO: have two BSSs? init_thread.bss and kernel-high.bss */ SECTIONS { @@ -61,31 +63,30 @@ SECTIONS * Boot Code + Boot Data *******************************************************************************************/ - __BOOT_START = .; - KEEP(*(.text.main.entry)) *(.text.boot) *(.data.boot) . = ALIGN(PAGE_SIZE); - __BOOT_END = .; /* Here the boot code ends */ - ASSERT((__BOOT_END & PAGE_MASK) == 0, "End of boot code is not page aligned") + __init_thread_end = .; /* Here the boot code ends */ + ASSERT((__init_thread_end & PAGE_MASK) == 0, "End of boot code is not page aligned") + } + + /* Physical addresses */ + __nucleus_start = .; + __CODE_START = .; + __kernel_virt_addr_space_start = .; - __init_thread_end = .; + /* Kernel virtual address */ + . = KERNEL_VIRT_ADDR_BASE; + .text : AT(__nucleus_start) + { /******************************************************************************************* * Regular Kernel Code *******************************************************************************************/ - /* Physical addresses */ - __nucleus_start = .; - __CODE_START = .; - __kernel_virt_addr_space_start = .; - - /* Kernel virtual address */ - . = KERNEL_VIRT_ADDR_BASE; - *(.text*) . = ALIGN(2048); @@ -132,18 +133,16 @@ SECTIONS .bss (NOLOAD) : ALIGN(PAGE_SIZE) { - __BSS_START = .; + __BSS_START = LOADADDR(.bss); /* Physical address */ *(.bss*) - . = ALIGN(PAGE_SIZE); /* Align up to page size */ - __BSS_END = .; - __BSS_SIZE_U64S = (__BSS_END - __BSS_START) / 8; /* TODO: remove */ - } :segment_data + __BSS_END = __BSS_START + ALIGN(SIZEOF(.bss), PAGE_SIZE); /* Physical address */ + . = ALIGN(PAGE_SIZE); /* Align up to page size */ - __DATA_END = .; + __DATA_END = .; /* Virtual address */ /*********************************************************************************************** * MMIO Remap Reserved (8Mb) diff --git a/nucleus/src/main.rs b/nucleus/src/main.rs index 27bfff904..83c97312b 100644 --- a/nucleus/src/main.rs +++ b/nucleus/src/main.rs @@ -14,9 +14,6 @@ #![feature(format_args_nl)] #![feature(stmt_expr_attributes)] #![feature(slice_ptr_get)] -#![feature(default_free_fn)] -#![feature(const_fn_trait_bound)] -#![feature(nonnull_slice_from_raw_parts)] #![deny(missing_docs)] #![deny(warnings)] #![allow(unused)] @@ -27,7 +24,7 @@ use core::panic::PanicInfo; #[allow(unused_imports)] -use libconsole::{console::console, SerialOps}; +use libconsole::{SerialOps, console::console}; use { cfg_if::cfg_if, core::{cell::UnsafeCell, time::Duration}, @@ -37,7 +34,7 @@ use { // , time }; -libboot::entry!(kernel_init); +libboot::entry!(init_thread); /// Kernel early init code. /// `arch` crate is responsible for calling it. @@ -49,11 +46,23 @@ libboot::entry!(kernel_init); /// - MMU + Data caching must be activated at the earliest. Without it, any atomic operations, /// e.g. the yet-to-be-introduced spinlocks in the device drivers (which currently employ /// `IRQSafeNullLocks` instead of spinlocks), will fail to work (properly) on the `RPi` `SoCs`. -pub unsafe fn kernel_init() -> ! { - #[cfg(feature = "jtag")] - libmachine::debug::jtag::wait_debugger(); +pub unsafe fn init_thread() -> ! { + // Entered in EL2: - libexception::exception::handling_init(); + // TODO list + // - Enter kernel init in EL2 - this will be needed to set up kernel mappings + // - Print DTB + // - Print max RAM from DTB + // - Print kernel covered area + // - Print KERNEL_HIGH_BASE + // - Print kernel mappings size and attribs + // - Print init_thread covered area + // - Print init_thread mappings size + + // #[cfg(feature = "jtag")] + // libmachine::debug::jtag::wait_debugger(); + + // libexception::exception::handling_init(); // SAFETY: Not safe! let phys_kernel_tables_base_addr = match unsafe { libmemory::mmu::kernel_map_binary() } { @@ -69,32 +78,27 @@ pub unsafe fn kernel_init() -> ! { libmemory::mmu::post_enable_init(); + // After page tables are populated and MMU is on, switch to EL1, now kernel will already be higher-half mapped. + // SAFETY: Not safe! - if let Err(x) = unsafe { libplatform::platform::drivers::init() } { - panic!("Error initializing platform drivers: {}", x); - } + // if let Err(x) = unsafe { libplatform::platform::drivers::init() } { + // panic!("Error initializing platform drivers: {}", x); + // } // Initialize all device drivers. // SAFETY: Not safe! - unsafe { - libplatform::platform::drivers::driver_manager().init_drivers_and_irqs(); - } + // unsafe { + // libplatform::platform::drivers::driver_manager().init_drivers_and_irqs(); + // } // Unmask interrupts on the boot CPU core. - libexception::exception::asynchronous::local_irq_unmask(); + // libexception::exception::asynchronous::local_irq_unmask(); // Announce conclusion of the kernel_init() phase. - libkernel_state::state_manager().transition_to_single_core_main(); - - libconsole::init_logger(); + // libkernel_state::state_manager().transition_to_single_core_main(); - // Transition from unsafe to safe. - kernel_main() -} + // libconsole::init_logger(); -/// Safe kernel code. -// #[inline] -pub fn kernel_main() -> ! { // info!("{}", libkernel::version()); // info!("Booting on: {}", bsp::board_name()); @@ -149,13 +153,13 @@ fn panicked(info: &PanicInfo) -> ! { } fn print_mmu_state_and_features() { - memory::features::print_features(); + libmemory::arch::features::print_features(); } fn dump_memory_map() { // Output the memory map as we could derive from FDT and information about our loaded image // Use it to imagine how the memmap would look like in the end. - arch::memory::print_layout(); + // arch::memory::print_layout(); } //------------------------------------------------------------ diff --git a/targets/aarch64-metta-none-eabi.json b/targets/aarch64-metta-none-eabi.json index a7393a7d9..da8592636 100644 --- a/targets/aarch64-metta-none-eabi.json +++ b/targets/aarch64-metta-none-eabi.json @@ -17,9 +17,6 @@ }, "os": "vesper", "panic-strategy": "abort", - "pre-link-args": { - "ld.lld": ["--print-gc-sections"] - }, "relocation-model": "static", "stack-probes": { "kind": "inline" From 5cf2e089b400427707979c0fec468922949fdda5 Mon Sep 17 00:00:00 2001 From: Berkus Decker Date: Wed, 21 Jan 2026 02:50:53 +0200 Subject: [PATCH 008/107] wip: early_print crash in qemu --- emulation/layout.zellij | 6 +++--- nucleus/src/main.rs | 27 ++++++++++++++++++--------- 2 files changed, 21 insertions(+), 12 deletions(-) diff --git a/emulation/layout.zellij b/emulation/layout.zellij index 8698da68c..f8e6b8b60 100644 --- a/emulation/layout.zellij +++ b/emulation/layout.zellij @@ -7,14 +7,14 @@ layout { } tab split_direction="Vertical" { pane split_direction="Vertical" { - pane command="bash" borderless=true close_on_exit=true { + pane command="bash" borderless=true close_on_exit=false { args "-c" "bash emulation/qemu_multi_uart.sh" } pane split_direction="Horizontal" { - pane command="bash" size="30%" close_on_exit=true { + pane command="bash" size="30%" close_on_exit=false { args "-c" "clear; echo -e \"\\033]0;MiniUart\\007\"; bash /dev/ptmx FIRST=1" } - pane command="bash" size="70%" close_on_exit=true { + pane command="bash" size="70%" close_on_exit=false { args "-c" "clear; echo -e \"\\033]0;PL011 Uart\\007\"; bash /dev/ptmx SECOND=1" } } diff --git a/nucleus/src/main.rs b/nucleus/src/main.rs index 83c97312b..928b6f52d 100644 --- a/nucleus/src/main.rs +++ b/nucleus/src/main.rs @@ -36,6 +36,16 @@ use { libboot::entry!(init_thread); +macro_rules! early_print { + // early_print!("a {} event", "log") + ($($arg:tt)+) => { + let mut buf = [0_u8; 4096]; // Increase this buffer size to allow dumping larger panic texts. + libqemu::semihosting::sys_write0_call( + libconsole::write_to::c_show(&mut buf, format_args!($($arg)+)).unwrap(), + ); + } +} + /// Kernel early init code. /// `arch` crate is responsible for calling it. /// @@ -49,15 +59,14 @@ libboot::entry!(init_thread); pub unsafe fn init_thread() -> ! { // Entered in EL2: - // TODO list - // - Enter kernel init in EL2 - this will be needed to set up kernel mappings - // - Print DTB - // - Print max RAM from DTB - // - Print kernel covered area - // - Print KERNEL_HIGH_BASE - // - Print kernel mappings size and attribs - // - Print init_thread covered area - // - Print init_thread mappings size + early_print!("Entered EL2"); + + early_print!("DTB at"); + early_print!("Max RAM size is"); + early_print!("Init thread image covers phys -:- identity mapped"); + early_print!("Init thread mapping tables filled in as - entries"); + early_print!("Kernel image covers phys -:- mapped to KERNEL_HIGH_BASE:-"); + early_print!("Kernel mapping tables filled in as - for kernel, as - for phys memory"); // #[cfg(feature = "jtag")] // libmachine::debug::jtag::wait_debugger(); From 9bc93e084235000e9c27726f44c220ded124199a Mon Sep 17 00:00:00 2001 From: Berkus Decker Date: Wed, 21 Jan 2026 03:27:23 +0200 Subject: [PATCH 009/107] wip: new linker script prototype --- .../src/platform/raspberrypi/linker/kernel.ld | 61 +++++++++---------- 1 file changed, 29 insertions(+), 32 deletions(-) diff --git a/libs/platform/src/platform/raspberrypi/linker/kernel.ld b/libs/platform/src/platform/raspberrypi/linker/kernel.ld index 1a7d20bd5..604d6b96b 100644 --- a/libs/platform/src/platform/raspberrypi/linker/kernel.ld +++ b/libs/platform/src/platform/raspberrypi/linker/kernel.ld @@ -5,14 +5,14 @@ * Original code distributed under MIT, additional changes are under BlueOak-1.0.0 */ -PAGE_SIZE = 64K; -PAGE_MASK = PAGE_SIZE - 1; +__PAGE_SIZE = 64K; +__PAGE_MASK = __PAGE_SIZE - 1; -__phys_mem_start = 0x0; -KERNEL_VIRT_ADDR_BASE = 0xffff000000000000; /* Nucleus is mapped here */ +__PHYS_MEM_START = 0x0; +__KERNEL_VIRT_ADDR_BASE = 0xffff000000000000; /* Nucleus is mapped here */ -__phys_load_addr = 0x80000; -ENTRY(__phys_load_addr); +__PHYS_LOAD_ADDR = 0x80000; +ENTRY(__PHYS_LOAD_ADDR); /* Flags: * 4 == R @@ -29,15 +29,15 @@ PHDRS segment_data PT_LOAD FLAGS(6); } -/* Symbols between __init_thread_start and __init_thread_end should be dropped after init is complete. +/* Symbols between __INIT_THREAD_START and __INIT_THREAD_END should be dropped after init is complete. Symbols between __CODE_START and __CODE_END are the kernel code. Symbols between __BSS_START and __BSS_END must be initialized to zero by startup code in the kernel. TODO: have two BSSs? init_thread.bss and kernel-high.bss */ SECTIONS { - . = __phys_mem_start; - __init_thread_start = .; + . = __PHYS_MEM_START; + __INIT_THREAD_START = .; /*********************************************************************************************** * Boot Core Stack @@ -46,18 +46,18 @@ SECTIONS { __STACK_BOTTOM = .; /* ^ */ /* | stack */ - . = __phys_load_addr; /* | growth AArch64 boot address is 0x80000, 4K-aligned */ + . = __PHYS_LOAD_ADDR; /* | growth AArch64 boot address is 0x80000, 4K-aligned */ /* | direction */ __STACK_TOP = .; /* | Stack grows from here towards 0x0. */ } :segment_boot_core_stack - ASSERT((__STACK_TOP & PAGE_MASK) == 0, "End of boot core stack is not page aligned") + ASSERT((__STACK_TOP & __PAGE_MASK) == 0, "End of boot core stack is not page aligned") /*********************************************************************************************** * Code + RO Data ***********************************************************************************************/ - .text : ALIGN(PAGE_SIZE) + .text : ALIGN(__PAGE_SIZE) { /******************************************************************************************* * Boot Code + Boot Data @@ -67,10 +67,10 @@ SECTIONS *(.text.boot) *(.data.boot) - . = ALIGN(PAGE_SIZE); + . = ALIGN(__PAGE_SIZE); - __init_thread_end = .; /* Here the boot code ends */ - ASSERT((__init_thread_end & PAGE_MASK) == 0, "End of boot code is not page aligned") + __INIT_THREAD_END = .; /* Here the boot code ends */ + ASSERT((__INIT_THREAD_END & __PAGE_MASK) == 0, "End of boot code is not page aligned") } /* Physical addresses */ @@ -79,7 +79,7 @@ SECTIONS __kernel_virt_addr_space_start = .; /* Kernel virtual address */ - . = KERNEL_VIRT_ADDR_BASE; + . = __KERNEL_VIRT_ADDR_BASE; .text : AT(__nucleus_start) { @@ -106,52 +106,49 @@ SECTIONS *(.rodata*) FILL(0x00) - . = ALIGN(PAGE_SIZE); /* Fill up to page size */ + . = ALIGN(__PAGE_SIZE); /* Fill up to page size */ __CODE_END = .; - ASSERT((__CODE_END & PAGE_MASK) == 0, "End of kernel code is not page aligned") + ASSERT((__CODE_END & __PAGE_MASK) == 0, "End of kernel code is not page aligned") } :segment_code /*********************************************************************************************** * Data + BSS ***********************************************************************************************/ - .data : ALIGN(PAGE_SIZE) + .data : ALIGN(__PAGE_SIZE) { __DATA_START = .; - ASSERT((__DATA_START & PAGE_MASK) == 0, "Start of kernel data is not page aligned") + ASSERT((__DATA_START & __PAGE_MASK) == 0, "Start of kernel data is not page aligned") *(.data*) FILL(0x00) - . = ALIGN(PAGE_SIZE); + . = ALIGN(__PAGE_SIZE); PHYS_KERNEL_TABLES_BASE_ADDR = .; KERNEL_TABLES = .; . += 0x20000; /* Random for now */ } :segment_data - .bss (NOLOAD) : ALIGN(PAGE_SIZE) + .bss (NOLOAD) : ALIGN(__PAGE_SIZE) { - __BSS_START = LOADADDR(.bss); /* Physical address */ - + __BSS_START = LOADADDR(.bss); /* LMA (PhysAddr) */ *(.bss*) - + . = ALIGN(__PAGE_SIZE); /* Align up to page size */ } :segment_data - __BSS_END = __BSS_START + ALIGN(SIZEOF(.bss), PAGE_SIZE); /* Physical address */ - - . = ALIGN(PAGE_SIZE); /* Align up to page size */ - __DATA_END = .; /* Virtual address */ + __BSS_END = __BSS_START + ALIGN(SIZEOF(.bss), __PAGE_SIZE); /* LMA, Physical address */ + __DATA_END = .; /* VMA, Virtual address */ /*********************************************************************************************** * MMIO Remap Reserved (8Mb) ***********************************************************************************************/ - __MMIO_REMAP_START = .; + __MMIO_REMAP_START = .; /* FIXME: VMA? Must be LMA perhaps, the point is to remap this */ . += 8 * 1024 * 1024; - __MMIO_REMAP_END = .; + __MMIO_REMAP_END = .; /* FIXME: VMA? Must be LMA perhaps, the point is to remap this */ - ASSERT((. & PAGE_MASK) == 0, "MMIO remap reservation is not page aligned") + ASSERT((. & __PAGE_MASK) == 0, "MMIO remap reservation is not page aligned") __kernel_virt_addr_space_size = . - __kernel_virt_addr_space_start; /* Physical end */ From 1e077ae36bda2cf2a5ec46a5a789f48b0b2cbabb Mon Sep 17 00:00:00 2001 From: Berkus Decker Date: Thu, 22 Jan 2026 02:49:17 +0200 Subject: [PATCH 010/107] wip: move nucleus to kernel before split --- Cargo.toml | 2 +- {nucleus => kernel}/MMU_Plan.md | 7 + {nucleus => kernel}/build.rs | 0 kernel/init_thread/Cargo.toml | 11 + kernel/init_thread/build.rs | 407 ++++++++++++++++++ kernel/init_thread/init_thread.ld | 70 +++ kernel/init_thread/src/boot_info.rs | 135 ++++++ {nucleus => kernel/nucleus}/Cargo.toml | 0 kernel/nucleus/build.rs | 5 + kernel/nucleus/nucleus.ld | 59 +++ {nucleus => kernel/nucleus}/src/main.rs | 0 {nucleus => kernel}/tests/common/mod.rs | 0 {nucleus => kernel}/tests/console.rs | 0 {nucleus => kernel}/tests/exception.rs | 0 {nucleus => kernel}/tests/locking.rs | 0 {nucleus => kernel}/tests/memory.rs | 0 {nucleus => kernel}/tests/nucleus.rs | 0 {nucleus => kernel}/tests/platform.rs | 0 libs/platform/Cargo.toml | 2 +- .../src/platform/raspberrypi/linker/kernel.ld | 2 +- 20 files changed, 697 insertions(+), 3 deletions(-) rename {nucleus => kernel}/MMU_Plan.md (53%) rename {nucleus => kernel}/build.rs (100%) create mode 100644 kernel/init_thread/Cargo.toml create mode 100644 kernel/init_thread/build.rs create mode 100644 kernel/init_thread/init_thread.ld create mode 100644 kernel/init_thread/src/boot_info.rs rename {nucleus => kernel/nucleus}/Cargo.toml (100%) create mode 100644 kernel/nucleus/build.rs create mode 100644 kernel/nucleus/nucleus.ld rename {nucleus => kernel/nucleus}/src/main.rs (100%) rename {nucleus => kernel}/tests/common/mod.rs (100%) rename {nucleus => kernel}/tests/console.rs (100%) rename {nucleus => kernel}/tests/exception.rs (100%) rename {nucleus => kernel}/tests/locking.rs (100%) rename {nucleus => kernel}/tests/memory.rs (100%) rename {nucleus => kernel}/tests/nucleus.rs (100%) rename {nucleus => kernel}/tests/platform.rs (100%) diff --git a/Cargo.toml b/Cargo.toml index 296f01be1..c87bfc686 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [workspace] members = [ - "nucleus", + "kernel", "bin/chainboot", "bin/chainofcommand", # Libraries diff --git a/nucleus/MMU_Plan.md b/kernel/MMU_Plan.md similarity index 53% rename from nucleus/MMU_Plan.md rename to kernel/MMU_Plan.md index e8dc11deb..e3471b0b3 100644 --- a/nucleus/MMU_Plan.md +++ b/kernel/MMU_Plan.md @@ -15,3 +15,10 @@ Steps: - Print kernel mappings size and attribs - Print init_thread covered area - Print init_thread mappings size + + + +Whatever kernel links must also be located in high-mem mapping, so we cannot share this code with init_thread at all! +This means it's probably sensible to build kernel as a separate ELF file linked entirely high, then merge it with the init_thread binary through specially-named sections; there should be no symbol resolution across two binaries, so the nucleus image is solely pulled via it's PHDRS (but we need to place the BSS which will be erased by the init_thread before turning the MMU on) + +See gh:metta-systems/kernel-embed-prototype for an outline of this approach - copy it here and lets go. diff --git a/nucleus/build.rs b/kernel/build.rs similarity index 100% rename from nucleus/build.rs rename to kernel/build.rs diff --git a/kernel/init_thread/Cargo.toml b/kernel/init_thread/Cargo.toml new file mode 100644 index 000000000..f577f1c00 --- /dev/null +++ b/kernel/init_thread/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "init_thread" +version = "0.1.0" +edition = "2024" + +[dependencies] + +[build-dependencies] +goblin = "0.10" +build-rs = "0.3" +build-print = "1.0" diff --git a/kernel/init_thread/build.rs b/kernel/init_thread/build.rs new file mode 100644 index 000000000..e46be17eb --- /dev/null +++ b/kernel/init_thread/build.rs @@ -0,0 +1,407 @@ +// Parse nucleus ELF and extract sections + +use { + build_print::info, + build_rs::output, + goblin::elf::{Elf, program_header::PT_LOAD, section_header::SHT_NOBITS}, + std::{ + env, + fs::{self, File}, + io::Read, + path::Path, + }, +}; + +fn main() { + let kernel_elf_path = "../target/aarch64-unknown-none/release/kernel"; // must be passed-in as input? + + output::rerun_if_changed(kernel_elf_path); + output::rerun_if_changed("build.rs"); + output::rustc_link_arg( + format!("--script={}/init_thread.ld", env!("CARGO_MANIFEST_DIR")).as_ref(), + ); + + let out_dir = env::var("OUT_DIR").unwrap(); + let out_path = Path::new(&out_dir); + + // Read kernel ELF + let mut elf_bytes = Vec::new(); + File::open(kernel_elf_path) + .expect("Failed to open kernel ELF - build kernel first with: cd ../kernel && cargo build --release") + .read_to_end(&mut elf_bytes) + .expect("Failed to read kernel ELF"); + + // Parse with goblin + let elf = Elf::parse(&elf_bytes).expect("Failed to parse kernel ELF"); + + // Verify it's what we expect + assert!(elf.is_64, "Kernel must be ELF64"); + assert_eq!( + elf.header.e_machine, + goblin::elf::header::EM_AARCH64, + "Kernel must be AArch64" + ); + + // Extract section information + let sections = extract_sections(&elf, &elf_bytes, out_path); + + // Generate Rust code + generate_rust_code(§ions, &elf, out_path); +} + +/// Extracted section with all metadata needed for loading +#[derive(Debug)] +struct ExtractedSection { + name: String, + /// Virtual address in kernel's address space (higher-half) + virt_addr: u64, + /// Size in memory + mem_size: usize, + /// Size in file (0 for BSS) + file_size: usize, + /// Required alignment + alignment: u64, + /// Permissions + readable: bool, + writable: bool, + executable: bool, + /// Is this a NOBITS section (BSS)? + is_nobits: bool, + /// Output binary file name (None for BSS) + bin_file: Option, +} + +/// Exception vector table information +#[derive(Debug)] +struct VectorTableInfo { + /// Virtual address of the vector table + virt_addr: u64, + /// Size of the vector table (should be 0x800 = 2048 bytes) + size: usize, + /// Alignment requirement (must be 2KB aligned for VBAR) + alignment: u64, +} + +/// All extracted kernel metadata +#[derive(Debug)] +struct KernelSections { + /// Virtual base address (start of kernel in virtual memory) + virt_base: u64, + /// Loadable sections (text, rodata, data) + load_sections: Vec, + /// BSS section (separate because it has no file content) + bss_section: Option, + /// Exception vector table info + vector_table: Option, +} + +fn extract_sections(elf: &Elf, elf_bytes: &[u8], out_path: &Path) -> KernelSections { + let mut load_sections = Vec::new(); + let mut bss_section = None; + let mut vector_table = None; + let mut virt_base = u64::MAX; + + // First pass: find virtual base from program headers + for ph in &elf.program_headers { + if ph.p_type == PT_LOAD { + virt_base = virt_base.min(ph.p_vaddr); + } + } + + // Build a map of section names to their info + let section_info: Vec<_> = elf + .section_headers + .iter() + .filter_map(|sh| { + let name = elf.shdr_strtab.get_at(sh.sh_name)?; + Some((name, sh)) + }) + .collect(); + + // Extract relevant sections + for (name, sh) in §ion_info { + // Skip sections we don't care about + if !matches!(*name, ".text" | ".rodata" | ".data" | ".bss") { + continue; + } + + let is_nobits = sh.sh_type == SHT_NOBITS; + + // Determine permissions from section flags + let readable = true; + let writable = sh.sh_flags & goblin::elf::section_header::SHF_WRITE as u64 != 0; + let executable = sh.sh_flags & goblin::elf::section_header::SHF_EXECINSTR as u64 != 0; + + let section = ExtractedSection { + name: name.to_string(), + virt_addr: sh.sh_addr, + mem_size: sh.sh_size as usize, + file_size: if is_nobits { 0 } else { sh.sh_size as usize }, + alignment: sh.sh_addralign, + readable, + writable, + executable, + is_nobits, + bin_file: None, + }; + + if is_nobits { + bss_section = Some(section); + } else { + load_sections.push(section); + } + } + + // Try to find vectors via symbol + vector_table = find_vector_table_from_symbols(elf); + + // Sort load sections by virtual address + load_sections.sort_by_key(|s| s.virt_addr); + + // Extract binary content for each loadable section + for section in &mut load_sections { + let sh = section_info + .iter() + .find(|(name, _)| *name == section.name) + .map(|(_, sh)| *sh) + .unwrap(); + + let start = sh.sh_offset as usize; + let end = start + sh.sh_size as usize; + let content = &elf_bytes[start..end]; + + let bin_filename = format!("kernel_{}.bin", section.name.trim_start_matches('.')); + let bin_path = out_path.join(&bin_filename); + fs::write(&bin_path, content) + .unwrap_or_else(|e| panic!("Failed to write {}: {}", bin_filename, e)); + + section.bin_file = Some(bin_filename); + + info!( + "Extracted {}: vaddr=0x{:016X}, size=0x{:X}, align={}, perms={}{}{}", + section.name, + section.virt_addr, + section.mem_size, + section.alignment, + if section.readable { "R" } else { "-" }, + if section.writable { "W" } else { "-" }, + if section.executable { "X" } else { "-" }, + ); + } + + if let Some(ref bss) = bss_section { + info!( + "BSS section: vaddr=0x{:016X}, size=0x{:X}, align={}", + bss.virt_addr, bss.mem_size, bss.alignment, + ); + } + + KernelSections { + virt_base, + load_sections, + bss_section, + vector_table, + } +} + +/// Try to find vector table location from symbols +fn find_vector_table_from_symbols(elf: &Elf) -> Option { + // Common symbol names for exception vectors + const VECTOR_SYMBOL: &str = "__vectors"; + + for sym in &elf.syms { + if let Some(name) = elf.strtab.get_at(sym.st_name) + && VECTOR_SYMBOL == name + { + info!( + "Found vector table symbol '{}': vaddr=0x{:016X}, size=0x{:X}", + name, sym.st_value, sym.st_size + ); + + // Verify 2KB alignment + if sym.st_value & 0x7FF != 0 { + info!( + "Vector table symbol at 0x{:016X} is not 2KB aligned!", + sym.st_value + ); + } + + return Some(VectorTableInfo { + virt_addr: sym.st_value, + // If size is 0, assume standard size of 0x800 + size: if sym.st_size > 0 { + sym.st_size as usize + } else { + 0x800 + }, + alignment: 2048, // VBAR requirement + }); + } + } + + output::error("No vector table symbol found! Kernel must define __vectors symbol"); + + None +} + +fn generate_rust_code(sections: &KernelSections, _elf: &Elf, out_path: &Path) { + let mut code = String::new(); + + // Header + code.push_str( + r#"// Auto-generated kernel section metadata and binary includes +// DO NOT EDIT - Generated by build.rs + +#[allow(unused)] +use crate::{ + loader::{BssSectionMeta, KernelImageInfo, KernelSectionMeta, LoadableSection, VectorTableMeta}, + memory::MemoryPermissions +}; + +"#, + ); + + // Generate binary includes + code.push_str("// ═══════════════════════════════════════════════════════════════\n"); + code.push_str("// Binary section data (included at compile time)\n"); + code.push_str("// ═══════════════════════════════════════════════════════════════\n\n"); + + for section in §ions.load_sections { + let const_name = format!( + "KERNEL_{}_BIN", + section.name.trim_start_matches('.').to_uppercase() + ); + let bin_file = section.bin_file.as_ref().unwrap(); + + code.push_str(&format!( + "static {}: &[u8] = include_bytes!(concat!(env!(\"OUT_DIR\"), \"/{}\"));\n", + const_name, bin_file + )); + } + + code.push('\n'); + + // Generate section metadata constants + code.push_str("// ═══════════════════════════════════════════════════════════════\n"); + code.push_str("// Section metadata\n"); + code.push_str("// ═══════════════════════════════════════════════════════════════\n\n"); + + for section in §ions.load_sections { + let const_name = format!( + "{}_META", + section.name.trim_start_matches('.').to_uppercase() + ); + + code.push_str(&format!( + r#"pub const {}: KernelSectionMeta = KernelSectionMeta {{ + name: "{}", + virt_addr: 0x{:016X}, + size: 0x{:X}, + alignment: 0x{:X}, + permissions: MemoryPermissions {{ readable: {}, writable: {}, executable: {} }}, +}}; + +"#, + const_name, + section.name, + section.virt_addr, + section.mem_size, + section.alignment, + section.readable, + section.writable, + section.executable, + )); + } + + // BSS metadata + if let Some(ref bss) = sections.bss_section { + code.push_str(&format!( + r#"pub const BSS_META: BssSectionMeta = BssSectionMeta {{ + virt_addr: 0x{:016X}, + size: 0x{:X}, + alignment: 0x{:X}, +}}; + +"#, + bss.virt_addr, bss.mem_size, bss.alignment, + )); + } + + // Vector table metadata + code.push_str("// ═══════════════════════════════════════════════════════════════\n"); + code.push_str("// Exception vector table\n"); + code.push_str("// ═══════════════════════════════════════════════════════════════\n\n"); + + if let Some(ref vectors) = sections.vector_table { + code.push_str(&format!( + r#"pub const VECTORS_META: VectorTableMeta = VectorTableMeta {{ + virt_addr: 0x{:016X}, + size: 0x{:X}, + alignment: 0x{:X}, +}}; + +"#, + vectors.virt_addr, vectors.size, vectors.alignment, + )); + } + + // Generate loadable sections array + code.push_str("// ═══════════════════════════════════════════════════════════════\n"); + code.push_str("// Combined section array\n"); + code.push_str("// ═══════════════════════════════════════════════════════════════\n\n"); + + code.push_str("static LOADABLE_SECTIONS: &[LoadableSection] = &[\n"); + for section in §ions.load_sections { + let meta_name = format!( + "{}_META", + section.name.trim_start_matches('.').to_uppercase() + ); + let data_name = format!( + "KERNEL_{}_BIN", + section.name.trim_start_matches('.').to_uppercase() + ); + + code.push_str(&format!( + " LoadableSection {{ meta: {}, data: {} }},\n", + meta_name, data_name + )); + } + code.push_str("];\n\n"); + + // Generate main kernel info + code.push_str(&format!( + r#"/// Complete kernel image information +pub static KERNEL: KernelImageInfo = KernelImageInfo {{ + virt_base: 0x{:016X}, + sections: LOADABLE_SECTIONS, + bss: {}, + vectors: {}, +}}; + +"#, + sections.virt_base, + if sections.bss_section.is_some() { + "Some(BSS_META)" + } else { + "None" + }, + if sections.vector_table.is_some() { + "Some(VECTORS_META)" + } else { + "None" + }, + )); + + // Write generated code + let dest_path = out_path.join("kernel_sections.rs"); + fs::write(&dest_path, code).expect("Failed to write generated code"); + + info!("Generated: {}", dest_path.display()); + info!("Kernel virtual base: 0x{:016X}", sections.virt_base); + if let Some(ref v) = sections.vector_table { + info!( + "Vector table: 0x{:016X} (size: 0x{:X})", + v.virt_addr, v.size + ); + } +} diff --git a/kernel/init_thread/init_thread.ld b/kernel/init_thread/init_thread.ld new file mode 100644 index 000000000..e853ec239 --- /dev/null +++ b/kernel/init_thread/init_thread.ld @@ -0,0 +1,70 @@ +/* init_thread/init.ld - Identity-mapped init thread */ + +ENTRY(_start) + +/* Physical load address - typical for RPi4 */ +INIT_BASE = 0x80000; + +SECTIONS +{ + . = INIT_BASE; + + __init_start = .; + + /* Entry point and early code */ + .text.boot ALIGN(4K) : + { + *(.text.boot) + } + + .text ALIGN(4K) : + { + *(.text .text.*) + } + + .rodata ALIGN(4K) : + { + *(.rodata .rodata.*) + + /* Embedded kernel will be here via include_bytes! */ + } + + .data ALIGN(4K) : + { + *(.data .data.*) + } + + .bss ALIGN(4K) : + { + __bss_start = .; + *(.bss .bss.*) + *(COMMON) + __bss_end = .; + } + + . = ALIGN(4K); + __init_end = .; + + /* Stack for init thread (grows down) */ + . = ALIGN(16); + __stack_bottom = .; + . += 64K; + __stack_top = .; + + /* Page tables - we allocate space for them here */ + . = ALIGN(4K); + __page_tables_start = .; + . += 256K; /* Space for page tables */ + __page_tables_end = .; + + /* Free memory starts here - used for kernel placement */ + . = ALIGN(2M); /* 2MB aligned for huge pages if desired */ + __free_memory_start = .; + + /DISCARD/ : + { + *(.comment) + *(.note*) + *(.eh_frame*) + } +} diff --git a/kernel/init_thread/src/boot_info.rs b/kernel/init_thread/src/boot_info.rs new file mode 100644 index 000000000..cf41af878 --- /dev/null +++ b/kernel/init_thread/src/boot_info.rs @@ -0,0 +1,135 @@ +#![allow(dead_code)] + +use crate::{memory::PhysAddr, println, sync}; + +#[derive(Default, Copy, Clone)] +struct BootInfoMemRegion { + pub start: PhysAddr, + pub end: PhysAddr, +} + +impl BootInfoMemRegion { + pub const fn new() -> BootInfoMemRegion { + BootInfoMemRegion { + start: PhysAddr::zero(), + end: PhysAddr::zero(), + } + } + + pub fn size(&self) -> u64 { + self.end - self.start + } + + pub fn is_empty(&self) -> bool { + self.start == self.end + } +} + +const NUM_MEM_REGIONS: usize = 16; + +pub enum BootInfoError { + NoFreeMemRegions, +} + +#[derive(Default)] +struct BootInfo { + pub regions: [BootInfoMemRegion; NUM_MEM_REGIONS], + pub max_slot_pos: usize, +} + +impl BootInfo { + pub const fn new() -> BootInfo { + BootInfo { + regions: [BootInfoMemRegion::new(); NUM_MEM_REGIONS], + max_slot_pos: 0, + } + } + + pub fn insert_region(&mut self, reg: BootInfoMemRegion) -> Result<(), BootInfoError> { + if reg.is_empty() { + return Ok(()); + } + assert!(reg.start <= reg.end); + for region in self.regions.iter_mut() { + if region.is_empty() { + *region = reg; + return Ok(()); + } + } + return Err(BootInfoError::NoFreeMemRegions); + } + + pub fn alloc_region(&mut self, size_bits: usize) -> Result { + let mut reg_index: usize = 0; + let mut reg: BootInfoMemRegion = BootInfoMemRegion::new(); + let mut rem_small: BootInfoMemRegion = BootInfoMemRegion::new(); + let mut rem_large: BootInfoMemRegion = BootInfoMemRegion::new(); + /* + * Search for a free mem region that will be the best fit for an allocation. We favour allocations + * that are aligned to either end of the region. If an allocation must split a region we favour + * an unbalanced split. In both cases we attempt to use the smallest region possible. In general + * this means we aim to make the size of the smallest remaining region smaller (ideally zero) + * followed by making the size of the largest remaining region smaller. + */ + for (i, reg_iter) in self.regions.iter().enumerate() { + let mut new_reg: BootInfoMemRegion = BootInfoMemRegion::new(); + + /* Determine whether placing the region at the start or the end will create a bigger left over region */ + if reg_iter.start.aligned_up(1usize << size_bits) - reg_iter.start + < reg_iter.end - reg_iter.end.aligned_down(1usize << size_bits) + { + new_reg.start = reg_iter.start.aligned_up(1usize << size_bits); + new_reg.end = new_reg.start + (1u64 << size_bits); + } else { + new_reg.end = reg_iter.end.aligned_down(1usize << size_bits); + new_reg.start = new_reg.end - (1u64 << size_bits); + } + if new_reg.end > new_reg.start + && new_reg.start >= reg_iter.start + && new_reg.end <= reg_iter.end + { + let mut new_rem_small: BootInfoMemRegion = BootInfoMemRegion::new(); + let mut new_rem_large: BootInfoMemRegion = BootInfoMemRegion::new(); + + if new_reg.start - reg_iter.start < reg_iter.end - new_reg.end { + new_rem_small.start = reg_iter.start; + new_rem_small.end = new_reg.start; + new_rem_large.start = new_reg.end; + new_rem_large.end = reg_iter.end; + } else { + new_rem_large.start = reg_iter.start; + new_rem_large.end = new_reg.start; + new_rem_small.start = new_reg.end; + new_rem_small.end = reg_iter.end; + } + if reg.is_empty() + || (new_rem_small.size() < rem_small.size()) + || (new_rem_small.size() == rem_small.size() + && new_rem_large.size() < rem_large.size()) + { + reg = new_reg; + rem_small = new_rem_small; + rem_large = new_rem_large; + reg_index = i; + } + } + } + if reg.is_empty() { + panic!("Kernel init failed: not enough memory\n"); + } + /* Remove the region in question */ + self.regions[reg_index] = BootInfoMemRegion::new(); + /* Add the remaining regions in largest to smallest order */ + self.insert_region(rem_large)?; + if self.insert_region(rem_small).is_err() { + println!( + "BootInfo::alloc_region(): wasted {} bytes due to alignment, try to increase NUM_MEM_REGIONS", + rem_small.size() + ); + } + Ok(reg.start) + } +} + +#[link_section = ".data.boot"] // @todo put zero-initialized stuff to .bss.boot! +static BOOT_INFO: sync::NullLock = sync::NullLock::new(BootInfo::new()); diff --git a/nucleus/Cargo.toml b/kernel/nucleus/Cargo.toml similarity index 100% rename from nucleus/Cargo.toml rename to kernel/nucleus/Cargo.toml diff --git a/kernel/nucleus/build.rs b/kernel/nucleus/build.rs new file mode 100644 index 000000000..aeed8650e --- /dev/null +++ b/kernel/nucleus/build.rs @@ -0,0 +1,5 @@ +fn main() { + build_rs::output::rustc_link_arg( + format!("--script={}/kernel.ld", env!("CARGO_MANIFEST_DIR")).as_ref(), + ); +} diff --git a/kernel/nucleus/nucleus.ld b/kernel/nucleus/nucleus.ld new file mode 100644 index 000000000..9c8660625 --- /dev/null +++ b/kernel/nucleus/nucleus.ld @@ -0,0 +1,59 @@ +/* kernel/kernel.ld - Higher-half kernel with vector table */ + +ENTRY(_kernel_entry) + +KERNEL_VIRT_BASE = 0xFFFF000000000000; + +SECTIONS +{ + . = KERNEL_VIRT_BASE; + + __kernel_start = .; + + /* Code section */ + .text : AT(ADDR(.text) - KERNEL_VIRT_BASE) ALIGN(4K) + { + __text_start = .; + *(.text.entry) + *(.text .text.*) + . = ALIGN(2K); /* Ensure full 2KB for vector table */ + KEEP(*(.vectors)) + . = ALIGN(2K); /* Ensure full 2KB for vector table */ + __text_end = .; + } + + /* Read-only data */ + .rodata : AT(ADDR(.rodata) - KERNEL_VIRT_BASE) ALIGN(4K) + { + __rodata_start = .; + *(.rodata .rodata.*) + __rodata_end = .; + } + + /* Initialized data */ + .data : AT(ADDR(.data) - KERNEL_VIRT_BASE) ALIGN(4K) + { + __data_start = .; + *(.data .data.*) + __data_end = .; + } + + /* BSS */ + .bss : AT(ADDR(.bss) - KERNEL_VIRT_BASE) ALIGN(4K) + { + __bss_start = .; + *(.bss .bss.*) + *(COMMON) + __bss_end = .; + } + + . = ALIGN(4K); + __kernel_end = .; + + /DISCARD/ : + { + *(.comment) + *(.note*) + *(.eh_frame*) + } +} diff --git a/nucleus/src/main.rs b/kernel/nucleus/src/main.rs similarity index 100% rename from nucleus/src/main.rs rename to kernel/nucleus/src/main.rs diff --git a/nucleus/tests/common/mod.rs b/kernel/tests/common/mod.rs similarity index 100% rename from nucleus/tests/common/mod.rs rename to kernel/tests/common/mod.rs diff --git a/nucleus/tests/console.rs b/kernel/tests/console.rs similarity index 100% rename from nucleus/tests/console.rs rename to kernel/tests/console.rs diff --git a/nucleus/tests/exception.rs b/kernel/tests/exception.rs similarity index 100% rename from nucleus/tests/exception.rs rename to kernel/tests/exception.rs diff --git a/nucleus/tests/locking.rs b/kernel/tests/locking.rs similarity index 100% rename from nucleus/tests/locking.rs rename to kernel/tests/locking.rs diff --git a/nucleus/tests/memory.rs b/kernel/tests/memory.rs similarity index 100% rename from nucleus/tests/memory.rs rename to kernel/tests/memory.rs diff --git a/nucleus/tests/nucleus.rs b/kernel/tests/nucleus.rs similarity index 100% rename from nucleus/tests/nucleus.rs rename to kernel/tests/nucleus.rs diff --git a/nucleus/tests/platform.rs b/kernel/tests/platform.rs similarity index 100% rename from nucleus/tests/platform.rs rename to kernel/tests/platform.rs diff --git a/libs/platform/Cargo.toml b/libs/platform/Cargo.toml index cc3142a69..30d35c482 100644 --- a/libs/platform/Cargo.toml +++ b/libs/platform/Cargo.toml @@ -53,7 +53,7 @@ ux = { workspace = true } [dev-dependencies] libtest = { workspace = true } -# Tests are offloaded to nucleus integration tests binary. +# Tests are offloaded to kernel integration tests binary. [lib] test = false diff --git a/libs/platform/src/platform/raspberrypi/linker/kernel.ld b/libs/platform/src/platform/raspberrypi/linker/kernel.ld index 604d6b96b..927a52c40 100644 --- a/libs/platform/src/platform/raspberrypi/linker/kernel.ld +++ b/libs/platform/src/platform/raspberrypi/linker/kernel.ld @@ -9,7 +9,7 @@ __PAGE_SIZE = 64K; __PAGE_MASK = __PAGE_SIZE - 1; __PHYS_MEM_START = 0x0; -__KERNEL_VIRT_ADDR_BASE = 0xffff000000000000; /* Nucleus is mapped here */ +__KERNEL_VIRT_ADDR_BASE = 0xffff000000000000; /* Nucleus is mapped here, init_thread is identity-mapped */ __PHYS_LOAD_ADDR = 0x80000; ENTRY(__PHYS_LOAD_ADDR); From c5cbcf4450f0d03ea8fb244895bb5049404dc8d5 Mon Sep 17 00:00:00 2001 From: Berkus Decker Date: Fri, 23 Jan 2026 13:55:01 +0200 Subject: [PATCH 011/107] wip: move impl from kernel-embed-prototype --- kernel/init_thread/src/bits.rs | 229 +++++++++++++++++++++ kernel/init_thread/src/boot.rs | 49 +++++ kernel/init_thread/src/el_switch.rs | 122 +++++++++++ kernel/init_thread/src/embed.rs | 2 + kernel/init_thread/src/loader.rs | 219 ++++++++++++++++++++ kernel/init_thread/src/main.rs | 106 ++++++++++ kernel/init_thread/src/memory.rs | 229 +++++++++++++++++++++ kernel/init_thread/src/paging.rs | 300 ++++++++++++++++++++++++++++ kernel/nucleus/nucleus.ld | 4 +- kernel/nucleus/src/main.rs | 230 +-------------------- kernel/nucleus/src/vectors.rs | 125 ++++++++++++ 11 files changed, 1385 insertions(+), 230 deletions(-) create mode 100644 kernel/init_thread/src/bits.rs create mode 100644 kernel/init_thread/src/boot.rs create mode 100644 kernel/init_thread/src/el_switch.rs create mode 100644 kernel/init_thread/src/embed.rs create mode 100644 kernel/init_thread/src/loader.rs create mode 100644 kernel/init_thread/src/main.rs create mode 100644 kernel/init_thread/src/memory.rs create mode 100644 kernel/init_thread/src/paging.rs create mode 100644 kernel/nucleus/src/vectors.rs diff --git a/kernel/init_thread/src/bits.rs b/kernel/init_thread/src/bits.rs new file mode 100644 index 000000000..0aea6c5e3 --- /dev/null +++ b/kernel/init_thread/src/bits.rs @@ -0,0 +1,229 @@ +libboot::entry!(init_thread); + +macro_rules! early_print { + // early_print!("a {} event", "log") + ($($arg:tt)+) => { + let mut buf = [0_u8; 4096]; // Increase this buffer size to allow dumping larger panic texts. + libqemu::semihosting::sys_write0_call( + libconsole::write_to::c_show(&mut buf, format_args!($($arg)+)).unwrap(), + ); + } +} + +/// Kernel early init code. +/// `arch` crate is responsible for calling it. +/// +/// # Safety +/// +/// - Only a single core must be active and running this function. +/// - The init calls in this function must appear in the correct order: +/// - MMU + Data caching must be activated at the earliest. Without it, any atomic operations, +/// e.g. the yet-to-be-introduced spinlocks in the device drivers (which currently employ +/// `IRQSafeNullLocks` instead of spinlocks), will fail to work (properly) on the `RPi` `SoCs`. +pub unsafe fn init_thread() -> ! { + // Entered in EL2: + + early_print!("Entered EL2"); + + early_print!("DTB at"); + early_print!("Max RAM size is"); + early_print!("Init thread image covers phys -:- identity mapped"); + early_print!("Init thread mapping tables filled in as - entries"); + early_print!("Kernel image covers phys -:- mapped to KERNEL_HIGH_BASE:-"); + early_print!("Kernel mapping tables filled in as - for kernel, as - for phys memory"); + + // #[cfg(feature = "jtag")] + // libmachine::debug::jtag::wait_debugger(); + + // libexception::exception::handling_init(); + + // SAFETY: Not safe! + let phys_kernel_tables_base_addr = match unsafe { libmemory::mmu::kernel_map_binary() } { + Err(string) => panic!("Error mapping kernel binary: {}", string), + Ok(addr) => addr, + }; + + // SAFETY: Not safe! + if let Err(e) = unsafe { libmemory::mmu::enable_mmu_and_caching(phys_kernel_tables_base_addr) } + { + panic!("Enabling MMU failed: {}", e); + } + + libmemory::mmu::post_enable_init(); + + // After page tables are populated and MMU is on, switch to EL1, now kernel will already be higher-half mapped. + + // SAFETY: Not safe! + // if let Err(x) = unsafe { libplatform::platform::drivers::init() } { + // panic!("Error initializing platform drivers: {}", x); + // } + + // Initialize all device drivers. + // SAFETY: Not safe! + // unsafe { + // libplatform::platform::drivers::driver_manager().init_drivers_and_irqs(); + // } + + // Unmask interrupts on the boot CPU core. + // libexception::exception::asynchronous::local_irq_unmask(); + + // Announce conclusion of the kernel_init() phase. + // libkernel_state::state_manager().transition_to_single_core_main(); + + // libconsole::init_logger(); + + // info!("{}", libkernel::version()); + // info!("Booting on: {}", bsp::board_name()); + + info!( + "{} version {}", + env!("CARGO_PKG_NAME"), + env!("CARGO_PKG_VERSION") + ); + info!( + "Booting on: {}", + libplatform::platform::BcmHost::board_name() + ); + + // info!("MMU online. Special regions:"); + // machine::platform::memory::mmu::virt_mem_layout().print_layout(); + + dump_memory_map(); + + let (_, privilege_level) = libexception::exception::current_privilege_level(); + info!("Current privilege level: {privilege_level}"); + + info!("Exception handling state:"); + libexception::exception::asynchronous::print_state(); + + info!( + "Architectural timer resolution: {} ns", + libtime::time::time_manager().resolution().as_nanos() + ); + + info!("Drivers loaded:"); + libplatform::platform::drivers::driver_manager().enumerate(); + + info!("Registered IRQ handlers:"); + libplatform::platform::exception::asynchronous::irq_manager().print_handler(); + + // Test a failing timer case. + libtime::time::time_manager().spin_for(Duration::from_nanos(1)); + + for _ in 0..3 { + info!("Spinning for 1 second"); + libtime::time::time_manager().spin_for(Duration::from_secs(1)); + } + + command_prompt(); + + reboot() +} + +fn print_mmu_state_and_features() { + libmemory::arch::features::print_features(); +} + +fn dump_memory_map() { + // Output the memory map as we could derive from FDT and information about our loaded image + // Use it to imagine how the memmap would look like in the end. + // arch::memory::print_layout(); +} + +//------------------------------------------------------------ +// Start a command prompt +//------------------------------------------------------------ +fn command_prompt() { + 'cmd_loop: loop { + let mut buf = [0_u8; 64]; + + match libconsole::console::command_prompt(&mut buf) { + // b"mmu" => init_mmu(), + b"feats" => print_mmu_state_and_features(), + // b"disp" => check_display_init(), + // b"trap" => check_data_abort_trap(), + // b"map" => machine::platform::memory::mmu::virt_mem_layout().print_layout(), + // b"led on" => set_led(true), + // b"led off" => set_led(false), + b"help" => print_help(), + b"end" => break 'cmd_loop, + x => warn!("[!] Unknown command {x:?}, try 'help'"), + } + } +} + +fn print_help() { + println!("Supported console commands:"); + println!(" mmu - initialize MMU"); + println!(" feats - print MMU state and supported features"); + #[cfg(not(feature = "noserial"))] + println!(" uart - try to reinitialize UART serial"); + // println!(" disp - try to init VC framebuffer and draw some text"); + println!(" trap - trigger and recover from a data abort exception"); + println!(" map - show kernel memory layout"); + // println!(" led [on|off] - change RPi LED status"); + println!(" end - leave console and reset board"); +} + +// fn set_led(enable: bool) { +// let mut mbox = Mailbox::<8>::default(); +// let index = mbox.request(); +// let index = mbox.set_led_on(index, enable); +// let mbox = mbox.end(index); +// +// mbox.call(channel::PropertyTagsArmToVc) +// .map_err(|e| { +// warn!("Mailbox call returned error {}", e); +// warn!("Mailbox contents: {:?}", mbox); +// }) +// .ok(); +// } + +fn reboot() -> ! { + cfg_if! { + if #[cfg(feature = "qemu")] { + info!("Bye, shutting down QEMU"); + libqemu::semihosting::exit_success() + } else { + // use machine::platform::raspberrypi::power::Power; + + info!("Bye, going to reset now"); + // Power::default().reset() + libcpu::endless_sleep() + } + } +} + +// fn check_display_init() { +// display_graphics() +// .map_err(|e| { +// warn!("Error in display: {}", e); +// }) +// .ok(); +// } +// +// fn display_graphics() -> Result<(), DrawError> { +// if let Ok(mut display) = VC::init_fb(800, 600, 32) { +// info!("Display created"); +// +// display.clear(Color::black()); +// info!("Display cleared"); +// +// display.rect(10, 10, 250, 250, Color::rgb(32, 96, 64)); +// display.draw_text(50, 50, "Hello there!", Color::rgb(128, 192, 255))?; +// +// let mut buf = [0u8; 64]; +// let s = machine::write_to::show(&mut buf, format_args!("Display width {}", display.width)); +// +// if s.is_err() { +// display.draw_text(50, 150, "Error displaying", Color::red())? +// } else { +// display.draw_text(50, 150, s.unwrap(), Color::white())? +// } +// +// display.draw_text(150, 50, "RED", Color::red())?; +// display.draw_text(160, 60, "GREEN", Color::green())?; +// display.draw_text(170, 70, "BLUE", Color::blue())?; +// } +// Ok(()) +// } diff --git a/kernel/init_thread/src/boot.rs b/kernel/init_thread/src/boot.rs new file mode 100644 index 000000000..34e4bf8c9 --- /dev/null +++ b/kernel/init_thread/src/boot.rs @@ -0,0 +1,49 @@ +// init_thread/src/boot.rs - Entry point in EL2 + +use core::arch::global_asm; + +global_asm!( + r#" +.section .text.boot +.global _start + +_start: + // Running in EL2 (assuming bootloader drops us here) + // x0 = DTB pointer (from bootloader) + + // Save DTB pointer + mov x19, x0 + + // Check we're in EL2 + mrs x0, CurrentEL + lsr x0, x0, #2 + cmp x0, #2 + b.ne .hang // Not EL2, hang + + // Set up EL2 stack + adrp x0, __stack_top + add x0, x0, :lo12:__stack_top + mov sp, x0 + + // Clear BSS for init_thread + adrp x0, __bss_start + add x0, x0, :lo12:__bss_start + adrp x1, __bss_end + add x1, x1, :lo12:__bss_end +.clear_bss: + cmp x0, x1 + b.ge .bss_done + stp xzr, xzr, [x0], #16 + b .clear_bss +.bss_done: + + // Call Rust init code with DTB pointer + mov x0, x19 + bl init_main + + // Should not return, but if it does... +.hang: + wfe + b .hang +"# +); diff --git a/kernel/init_thread/src/el_switch.rs b/kernel/init_thread/src/el_switch.rs new file mode 100644 index 000000000..8d12041ca --- /dev/null +++ b/kernel/init_thread/src/el_switch.rs @@ -0,0 +1,122 @@ +// init_thread/src/el_switch.rs - EL2 to EL1 transition with VBAR setup + +/// Configure and enable the MMU, set VBAR_EL1, then drop to EL1 +/// +/// # Arguments +/// +/// * `ttbr0` - Translation table base for low addresses (identity map) +/// * `ttbr1` - Translation table base for high addresses (kernel) +/// * `vbar` - Exception vector base address (physical, must be 2KB aligned) +/// * `entry_point` - Kernel entry point (virtual address) +/// * `stack_pointer` - Initial stack pointer for EL1 +/// +/// # Safety +/// +/// This function never returns to the caller. +#[inline(never)] +pub unsafe fn enable_mmu_and_drop_to_el1( + ttbr0: u64, + ttbr1: u64, + vbar: u64, + entry_point: u64, + stack_pointer: u64, +) -> ! { + // MAIR: Memory Attribute Indirection Register + // Index 0: Normal memory, Write-Back, Read/Write Allocate + // Index 1: Device-nGnRnE memory + let mair: u64 = 0xFF | (0x00 << 8); + + unsafe { + core::arch::asm!( + // ═══════════════════════════════════════════════════════════ + // STEP 1: Configure EL2 to allow EL1 operation + // ═══════════════════════════════════════════════════════════ + + // HCR_EL2: RW=1 means EL1 is AArch64 + "mov x0, #(1 << 31)", + "msr hcr_el2, x0", + + // ═══════════════════════════════════════════════════════════ + // STEP 2: Set up VBAR_EL1 (Exception Vector Base Address) + // ═══════════════════════════════════════════════════════════ + // + // The address must be 2KB aligned (bits [10:0] must be 0). + // We set the virtual address here since VBAR_EL1 is only + // used after MMU is enabled (exceptions before ERET would + // be taken at EL2, not EL1). + + "msr vbar_el1, {vbar}", + + // ═══════════════════════════════════════════════════════════ + // STEP 3: Configure EL1 MMU settings + // ═══════════════════════════════════════════════════════════ + + "msr mair_el1, {mair}", + + // TCR_EL1: Translation Control Register + "mov x0, #16", // T0SZ = 16 (48-bit VA for TTBR0) + "orr x0, x0, #(16 << 16)", // T1SZ = 16 (48-bit VA for TTBR1) + "orr x0, x0, #(0b10 << 30)", // TG1 = 4KB granule + "orr x0, x0, #(0b11 << 12)", // SH0 = Inner shareable + "orr x0, x0, #(0b11 << 28)", // SH1 = Inner shareable + "orr x0, x0, #(0b01 << 10)", // ORGN0 = Write-back + "orr x0, x0, #(0b01 << 26)", // ORGN1 = Write-back + "orr x0, x0, #(0b01 << 8)", // IRGN0 = Write-back + "orr x0, x0, #(0b01 << 24)", // IRGN1 = Write-back + "msr tcr_el1, x0", + + "msr ttbr0_el1, {ttbr0}", + "msr ttbr1_el1, {ttbr1}", + + // ═══════════════════════════════════════════════════════════ + // STEP 4: Prepare to drop to EL1 with MMU enabled + // ═══════════════════════════════════════════════════════════ + + // Enable MMU (takes effect after ERET) + "mrs x0, sctlr_el1", + "orr x0, x0, #1", // M = 1 + "orr x0, x0, #(1 << 2)", // C = 1 + "orr x0, x0, #(1 << 12)", // I = 1 + "msr sctlr_el1, x0", + + // SPSR_EL2: EL1h with DAIF masked + "mov x0, #0b0101", // EL1h + "orr x0, x0, #(0b1111 << 6)", // Mask DAIF + "msr spsr_el2, x0", + + // Set return address and stack + "msr elr_el2, {entry}", + "msr sp_el1, {sp}", + + "dsb sy", + "isb", + + // ═══════════════════════════════════════════════════════════ + // STEP 5: Drop to EL1 + // ═══════════════════════════════════════════════════════════ + "eret", + + mair = in(reg) mair, + ttbr0 = in(reg) ttbr0, + ttbr1 = in(reg) ttbr1, + vbar = in(reg) vbar, + entry = in(reg) entry_point, + sp = in(reg) stack_pointer, + options(noreturn, nostack) + ); + } +} + +/// Invalidate all TLB entries +#[inline(always)] +pub fn tlb_invalidate_all() { + unsafe { + core::arch::asm!( + "dsb ishst", + "tlbi vmalle1", + "dsb ish", + "isb", + options(nostack, preserves_flags) + ); + } +} diff --git a/kernel/init_thread/src/embed.rs b/kernel/init_thread/src/embed.rs new file mode 100644 index 000000000..9c49eeb14 --- /dev/null +++ b/kernel/init_thread/src/embed.rs @@ -0,0 +1,2 @@ +// Include generated types +include!(concat!(env!("OUT_DIR"), "/kernel_sections.rs")); diff --git a/kernel/init_thread/src/loader.rs b/kernel/init_thread/src/loader.rs new file mode 100644 index 000000000..33d528f9f --- /dev/null +++ b/kernel/init_thread/src/loader.rs @@ -0,0 +1,219 @@ +// init_thread/src/loader.rs + +use { + crate::{ + embed::KERNEL, + memory::{BootAllocator, KernelLayout, MemoryPermissions, PhysAddr, VirtAddr}, + }, + core::ptr, +}; + +/// Metadata for a kernel section +#[derive(Debug, Clone, Copy)] +pub struct KernelSectionMeta { + /// Section name (for debugging) + pub name: &'static str, + /// Virtual address in kernel's higher-half address space + pub virt_addr: u64, + /// Size of section in bytes + pub size: usize, + /// Required alignment in bytes + pub alignment: u64, + /// Memory protection permissions + pub permissions: MemoryPermissions, +} + +impl KernelSectionMeta { + /// Calculate offset from kernel virtual base + pub const fn offset_from_base(&self, virt_base: u64) -> u64 { + self.virt_addr - virt_base + } + + /// Calculate physical address given kernel physical base + pub const fn phys_addr(&self, kernel_phys_base: u64, kernel_virt_base: u64) -> u64 { + kernel_phys_base + self.offset_from_base(kernel_virt_base) + } + + /// Number of 4KB pages needed + pub const fn page_count(&self) -> usize { + self.size.div_ceil(0x1000) + } +} + +/// Exception vector table metadata +/// +/// The vector table must be 2KB aligned for VBAR_EL1. +/// It contains 16 entries × 128 bytes = 2048 bytes total. +#[derive(Debug, Clone, Copy)] +pub struct VectorTableMeta { + /// Virtual address of the vector table (higher-half) + pub virt_addr: u64, + /// Size of the vector table (typically 0x800 = 2048 bytes) + pub size: usize, + /// Alignment requirement (must be at least 2048 for VBAR) + pub alignment: u64, +} + +impl VectorTableMeta { + /// Calculate offset from kernel virtual base + pub const fn offset_from_base(&self, virt_base: u64) -> u64 { + self.virt_addr - virt_base + } + + /// Calculate physical address given kernel physical base + /// + /// This is the value to write to VBAR_EL1 before enabling MMU, + /// since at that point we're still using physical addresses. + pub const fn phys_addr(&self, kernel_phys_base: u64, kernel_virt_base: u64) -> u64 { + kernel_phys_base + self.offset_from_base(kernel_virt_base) // FIXME: dupe of the above same fns + } + + /// Verify the address meets VBAR alignment requirements + pub const fn is_properly_aligned(&self, addr: u64) -> bool { + addr & 0x7FF == 0 // Must be 2KB aligned + } +} + +/// Complete kernel image information +#[derive(Debug)] +pub struct KernelImageInfo { + /// Virtual base address (higher-half) -- FIXME: don't need this necessarily + pub virt_base: u64, + /// Loadable sections with their binary data + pub sections: &'static [LoadableSection], + /// BSS section metadata (no binary data - must be zeroed) + pub bss: Option, + /// Exception vector table metadata + pub vectors: Option, +} + +impl KernelImageInfo { + /// Total size needed for kernel in physical memory (all sections + BSS) + pub fn total_size(&self) -> usize { + let mut max_end: u64 = 0; + + for section in self.sections { + let end = section.meta.virt_addr + section.meta.size as u64; + max_end = max_end.max(end); + } + + if let Some(bss) = &self.bss { + let bss_end = bss.virt_addr + bss.size as u64; + max_end = max_end.max(bss_end); + } + + let size = (max_end - self.virt_base) as usize; + (size + 0xFFF) & !0xFFF // FIXME: aligned to a page size + } +} + +/// A loadable section with its binary content +#[derive(Debug)] +pub struct LoadableSection { + pub meta: KernelSectionMeta, + pub data: &'static [u8], // or Option<&'static [u8]>? +} + +pub fn load_kernel(allocator: &mut BootAllocator) -> Result { + let total_size = KERNEL.total_size(); + let total_pages = total_size.div_ceil(0x1000); + + // Allocate 2MB-aligned for potential huge page mapping -- FIXME: with this we can abandon the whole loaded imade and do ASLR easy + let phys_base = allocator + .alloc_aligned(total_pages * 0x1000, 2 * 1024 * 1024) + .ok_or("Failed to allocate memory for kernel")?; + + // Load each section + for section in KERNEL.sections { + load_section(section, phys_base)?; + } + + // Zero BSS section + if let Some(bss) = &KERNEL.bss { + zero_bss(bss, phys_base)?; + } + + memory_barrier(); + + // Build layout information + let bss_info = KERNEL.bss.map(|bss| { + let phys = PhysAddr::new(phys_base.as_u64() + bss.offset_from_base(KERNEL.virt_base)); + (phys, VirtAddr::new(bss.virt_addr), bss.size) + }); + + // Calculate vector table addresses + let vectors_info = KERNEL.vectors.map(|v| { + let phys = PhysAddr::new(phys_base.as_u64() + v.offset_from_base(KERNEL.virt_base)); + let virt = VirtAddr::new(v.virt_addr); + + // Verify alignment + if !virt.is_aligned(2048) { + panic!( + "Vector table virtual address 0x{:016X} is not 2KB aligned!", + virt.0 + ); + } + + (phys, virt) + }); + + Ok(KernelLayout { + phys_base, + virt_base: VirtAddr::new(KERNEL.virt_base), + total_size, + sections: KERNEL.sections, + bss_phys: bss_info.map(|(p, _, _)| p).unwrap_or(PhysAddr::new(0)), + bss_virt: bss_info.map(|(_, v, _)| v).unwrap_or(VirtAddr::new(0)), + bss_size: bss_info.map(|(_, _, s)| s).unwrap_or(0), + vectors_phys: vectors_info.map(|(p, _)| p), + vectors_virt: vectors_info.map(|(_, v)| v), + }) +} + +fn load_section(section: &LoadableSection, kernel_phys_base: PhysAddr) -> Result<(), &'static str> { + let offset = section.meta.offset_from_base(KERNEL.virt_base); + let dest_phys = PhysAddr::new(kernel_phys_base.as_u64() + offset); + + if !dest_phys.as_u64().is_multiple_of(section.meta.alignment) { + return Err("Section alignment violated"); + } + + unsafe { + ptr::copy_nonoverlapping( + section.data.as_ptr(), + dest_phys.as_mut_ptr::(), + section.data.len(), + ); + } + + if section.meta.size > section.data.len() { + let zero_start = PhysAddr::new(dest_phys.as_u64() + section.data.len() as u64); + let zero_size = section.meta.size - section.data.len(); + unsafe { + ptr::write_bytes(zero_start.as_mut_ptr::(), 0, zero_size); + } + } + + Ok(()) +} + +fn zero_bss(bss: &KernelSectionMeta, kernel_phys_base: PhysAddr) -> Result<(), &'static str> { + let offset = bss.offset_from_base(KERNEL.virt_base); + let dest_phys = PhysAddr::new(kernel_phys_base.as_u64() + offset); + + if !dest_phys.as_u64().is_multiple_of(bss.alignment) { + return Err("BSS alignment violated"); + } + + unsafe { + ptr::write_bytes(dest_phys.as_mut_ptr::(), 0, bss.size); + } + Ok(()) +} + +#[inline(always)] +pub fn memory_barrier() { + unsafe { + core::arch::asm!("dsb sy", "isb", options(nostack, preserves_flags)); + } +} diff --git a/kernel/init_thread/src/main.rs b/kernel/init_thread/src/main.rs new file mode 100644 index 000000000..42e867e04 --- /dev/null +++ b/kernel/init_thread/src/main.rs @@ -0,0 +1,106 @@ +// init_thread/src/main.rs + +#![no_std] +#![no_main] +#![allow(unused)] + +mod boot; +mod el_switch; +mod embed; +mod loader; +mod memory; +mod paging; + +use { + core::panic::PanicInfo, + memory::{BootAllocator, PhysAddr}, +}; + +unsafe extern "C" { + static __init_start: u8; + static __init_end: u8; + static __free_memory_start: u8; +} + +#[unsafe(no_mangle)] +pub extern "C" fn init_main(_dtb_ptr: *const u8) -> ! { + let init_start = unsafe { &__init_start as *const u8 as u64 }; + let init_end = unsafe { &__init_end as *const u8 as u64 }; + let free_start = unsafe { &__free_memory_start as *const u8 as u64 }; + + let memory_size = 256 * 1024 * 1024; + let mut allocator = BootAllocator::new(PhysAddr::new(free_start), memory_size); + + // ═══════════════════════════════════════════════════════════════ + // PHASE 1: Load kernel + // ═══════════════════════════════════════════════════════════════ + + let kernel_layout = loader::load_kernel(&mut allocator).expect("Failed to load kernel"); + + // ═══════════════════════════════════════════════════════════════ + // PHASE 2: Set up page tables + // ═══════════════════════════════════════════════════════════════ + + let mut mmu_setup = paging::MmuSetup::new(&mut allocator).expect("Failed to create MMU setup"); + + // Identity map init_thread + paging::create_identity_mapping( + &mut mmu_setup, + PhysAddr::new(init_start), + PhysAddr::new(init_end + 0x10000), + ) + .expect("Failed to create identity mapping"); + + // Create kernel mapping with per-section permissions + paging::create_kernel_mapping(&mut mmu_setup, &kernel_layout) + .expect("Failed to create kernel mapping"); + + // ═══════════════════════════════════════════════════════════════ + // PHASE 3: Prepare for EL1 + // ═══════════════════════════════════════════════════════════════ + + let ttbr0 = mmu_setup.ttbr0(); + let ttbr1 = mmu_setup.ttbr1(); + + // Get vector table virtual address for VBAR_EL1 + // VBAR is only used after MMU is enabled, so we set the virtual address directly + let vbar = kernel_layout + .vbar_el1_virt() + .expect("Kernel must define exception vector table (__vectors symbol)"); + + // Allocate EL1 stack + let el1_stack = allocator + .alloc_pages(16) + .expect("Failed to allocate EL1 stack"); + let el1_stack_top = el1_stack.as_u64() + 64 * 1024; + // FIXME: stack must be identity-mapped! + + // ═══════════════════════════════════════════════════════════════ + // PHASE 4: Enable MMU and drop to EL1 + // ═══════════════════════════════════════════════════════════════ + + unsafe { + el_switch::enable_mmu_and_drop_to_el1( + ttbr0, + ttbr1, + vbar, + init_thread_run as *const () as u64, + el1_stack_top, + ); + } +} + +#[panic_handler] +fn panic(_info: &PanicInfo) -> ! { + loop { + unsafe { + core::arch::asm!("wfe"); + } + } +} + +#[unsafe(no_mangle)] +pub extern "C" fn init_thread_run(_dtb_ptr: *const u8) -> ! { + // Run initial thread further in EL1, seting up the capDL etc. + panic!("continue system init here"); +} diff --git a/kernel/init_thread/src/memory.rs b/kernel/init_thread/src/memory.rs new file mode 100644 index 000000000..531064de9 --- /dev/null +++ b/kernel/init_thread/src/memory.rs @@ -0,0 +1,229 @@ +// Boot allocator, Section mapping, memory permissions + +use crate::loader::LoadableSection; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +#[repr(transparent)] +pub struct PhysAddr(pub u64); + +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +#[repr(transparent)] +pub struct VirtAddr(pub u64); + +impl PhysAddr { + pub const fn new(addr: u64) -> Self { + Self(addr) + } + pub const fn as_u64(self) -> u64 { + self.0 + } + pub const fn as_ptr(self) -> *const T { + self.0 as *const T + } + pub fn as_mut_ptr(self) -> *mut T { + self.0 as *mut T + } + pub const fn add(self, offset: u64) -> Self { + Self(self.0 + offset) + } + pub const fn align_up(self, align: u64) -> Self { + Self((self.0 + align - 1) & !(align - 1)) + } + pub const fn align_down(self, align: u64) -> Self { + Self(self.0 & !(align - 1)) + } + pub const fn is_aligned(self, align: u64) -> bool { + self.0 & (align - 1) == 0 + } +} + +impl VirtAddr { + pub const fn new(addr: u64) -> Self { + Self(addr) + } + pub const fn as_u64(self) -> u64 { + self.0 + } + pub const fn add(self, offset: u64) -> Self { + Self(self.0 + offset) + } + pub const fn sub(self, other: VirtAddr) -> u64 { + self.0 - other.0 + } + pub const fn is_higher_half(self) -> bool { + self.0 >= 0xFFFF_0000_0000_0000 + } +} + +pub struct BootAllocator { + current: PhysAddr, + end: PhysAddr, +} + +impl BootAllocator { + pub const fn new(start: PhysAddr, size: usize) -> Self { + Self { + current: start, + end: PhysAddr(start.0 + size as u64), + } + } + + pub fn alloc_pages(&mut self, count: usize) -> Option { + self.alloc_aligned(count * 4096, 4096) + } + + pub fn alloc_aligned(&mut self, size: usize, align: usize) -> Option { + let aligned = self.current.align_up(align as u64); + let new_current = PhysAddr(aligned.0 + size as u64); + if new_current > self.end { + return None; + } + self.current = new_current; + Some(aligned) + } + + pub fn current(&self) -> PhysAddr { + self.current + } + pub fn remaining(&self) -> usize { + (self.end.0 - self.current.0) as usize + } +} + +/// Memory protection flags +#[derive(Debug, Clone, Copy)] +pub struct MemoryPermissions { + pub readable: bool, + pub writable: bool, + pub executable: bool, +} + +impl MemoryPermissions { + /// Convert to AArch64 page table flags + pub const fn as_pte_flags(&self) -> u64 { + let mut flags = 0u64; + + // Access flag (must be set) + flags |= 1 << 10; // AF + + // Shareability (inner shareable for normal memory) + flags |= 0b11 << 8; // SH + + // Access permissions + if !self.writable { + flags |= 0b10 << 6; // AP[2:1] = read-only + } + + // Execute never flags + if !self.executable { + flags |= 1 << 53; // PXN + flags |= 1 << 54; // UXN + } + + flags + } +} + +/// Layout of the loaded kernel in physical memory +#[derive(Debug)] +pub struct KernelLayout { + /// Physical address where kernel is loaded + pub phys_base: PhysAddr, + /// Virtual base address (higher-half) + pub virt_base: VirtAddr, + /// Total size of kernel in physical memory + pub total_size: usize, + /// Section metadata (for page table setup) + pub sections: &'static [LoadableSection], + /// BSS physical address + pub bss_phys: PhysAddr, + /// BSS virtual address + pub bss_virt: VirtAddr, + /// BSS size + pub bss_size: usize, + /// Exception vector table physical address (for VBAR_EL1) + pub vectors_phys: Option, + /// Exception vector table virtual address + pub vectors_virt: Option, +} + +impl KernelLayout { + pub fn virt_to_phys(&self, virt: VirtAddr) -> PhysAddr { + let offset = virt.as_u64() - self.virt_base.as_u64(); + PhysAddr(self.phys_base.as_u64() + offset) + } + + /// Get the VBAR_EL1 value (virtual address for use after MMU enable) + /// + /// This is what the kernel would set VBAR_EL1 to after switching to + /// higher-half addresses. + pub fn vbar_el1_virt(&self) -> Option { + self.vectors_virt.map(|virt| { + assert!( + virt.as_u64() & 0x7FF == 0, + "VBAR_EL1 address 0x{:016X} must be 2KB aligned", + virt.0 + ); + virt.0 + }) + } + + pub fn iter_sections(&self) -> impl Iterator + '_ { + self.sections.iter().map(move |section| { + let offset = section.meta.offset_from_base(self.virt_base.as_u64()); + SectionMapping { + name: section.meta.name, + phys_start: PhysAddr::new(self.phys_base.as_u64() + offset), + virt_start: VirtAddr::new(section.meta.virt_addr), + size: section.meta.size, + permissions: section.meta.permissions, + } + }) + } + + pub fn bss_mapping(&self) -> Option { + if self.bss_size > 0 { + Some(SectionMapping { + name: ".bss", + phys_start: self.bss_phys, + virt_start: self.bss_virt, + size: self.bss_size, + permissions: MemoryPermissions { + readable: true, + writable: true, + executable: false, + }, + }) + } else { + None + } + } +} + +#[derive(Debug, Clone, Copy)] +pub struct SectionMapping { + pub name: &'static str, + pub phys_start: PhysAddr, + pub virt_start: VirtAddr, + pub size: usize, + pub permissions: MemoryPermissions, +} + +impl SectionMapping { + pub fn page_count(&self) -> usize { + self.size.div_ceil(0x1000) + } + + pub fn pages(&self) -> impl Iterator { + let phys_start = self.phys_start.as_u64(); + let virt_start = self.virt_start.as_u64(); + let count = self.page_count(); + (0..count).map(move |i| { + let offset = (i * 0x1000) as u64; + ( + PhysAddr::new(phys_start + offset), + VirtAddr::new(virt_start + offset), + ) + }) + } +} diff --git a/kernel/init_thread/src/paging.rs b/kernel/init_thread/src/paging.rs new file mode 100644 index 000000000..a222b1e57 --- /dev/null +++ b/kernel/init_thread/src/paging.rs @@ -0,0 +1,300 @@ +// init_thread/src/paging.rs - Page table management with section-aware mapping + +use { + crate::memory::{ + BootAllocator, KernelLayout, MemoryPermissions, PhysAddr, SectionMapping, VirtAddr, + }, + core::ptr, +}; + +/// Page table entry flags for AArch64 Stage 1 +pub mod flags { + pub const VALID: u64 = 1 << 0; + pub const TABLE: u64 = 1 << 1; + pub const PAGE: u64 = 1 << 1; + + // Access permissions (AP[2:1]) + pub const AP_RW_EL1: u64 = 0b00 << 6; + pub const AP_RO_EL1: u64 = 0b10 << 6; + + // Shareability + pub const SH_INNER: u64 = 0b11 << 8; + + // Access flag + pub const AF: u64 = 1 << 10; + + // Memory attributes index + pub const ATTR_NORMAL: u64 = 0 << 2; + pub const ATTR_DEVICE: u64 = 1 << 2; + + // Execute never + pub const PXN: u64 = 1 << 53; + pub const UXN: u64 = 1 << 54; + + // Block descriptor + pub const BLOCK: u64 = 0; +} + +/// Page table (512 entries × 8 bytes = 4KB) +#[repr(C, align(4096))] +pub struct PageTable { + entries: [u64; 512], +} + +impl PageTable { + pub const fn new() -> Self { + Self { entries: [0; 512] } + } +} + +/// Which TTBR to use +#[derive(Debug, Clone, Copy)] +pub enum Ttbr { + Ttbr0, + Ttbr1, +} + +/// MMU configuration builder +pub struct MmuSetup<'a> { + allocator: &'a mut BootAllocator, + ttbr0_l0: PhysAddr, + ttbr1_l0: PhysAddr, +} + +impl<'a> MmuSetup<'a> { + pub fn new(allocator: &'a mut BootAllocator) -> Result { + let ttbr0_l0 = allocator + .alloc_pages(1) + .ok_or("Failed to allocate TTBR0 L0 table")?; + + let ttbr1_l0 = allocator + .alloc_pages(1) + .ok_or("Failed to allocate TTBR1 L0 table")?; + + unsafe { + ptr::write_bytes(ttbr0_l0.as_mut_ptr::(), 0, 4096); + ptr::write_bytes(ttbr1_l0.as_mut_ptr::(), 0, 4096); + } + + Ok(Self { + allocator, + ttbr0_l0, + ttbr1_l0, + }) + } + + /// Map a 4KB page with specific permissions + pub fn map_page( + &mut self, + ttbr: Ttbr, + virt: VirtAddr, + phys: PhysAddr, + perms: MemoryPermissions, + ) -> Result<(), &'static str> { + let pte_flags = perms.as_pte_flags() | flags::ATTR_NORMAL; + self.map_page_with_flags(ttbr, virt, phys, pte_flags) + } + + /// Map a 4KB page with raw PTE flags + fn map_page_with_flags( + &mut self, + ttbr: Ttbr, + virt: VirtAddr, + phys: PhysAddr, + pte_flags: u64, + ) -> Result<(), &'static str> { + let l0_phys = match ttbr { + Ttbr::Ttbr0 => self.ttbr0_l0, + Ttbr::Ttbr1 => self.ttbr1_l0, + }; + + let va = virt.as_u64(); + let l0_idx = ((va >> 39) & 0x1FF) as usize; + let l1_idx = ((va >> 30) & 0x1FF) as usize; + let l2_idx = ((va >> 21) & 0x1FF) as usize; + let l3_idx = ((va >> 12) & 0x1FF) as usize; + + let l1_phys = self.ensure_table(l0_phys, l0_idx)?; + let l2_phys = self.ensure_table(l1_phys, l1_idx)?; + let l3_phys = self.ensure_table(l2_phys, l2_idx)?; + + let l3_table = unsafe { &mut *(l3_phys.as_mut_ptr::()) }; + l3_table.entries[l3_idx] = phys.as_u64() | flags::VALID | flags::PAGE | pte_flags; + + Ok(()) + } + + /// Map a 2MB block with specific permissions + pub fn map_block_2mb( + &mut self, + ttbr: Ttbr, + virt: VirtAddr, + phys: PhysAddr, + perms: MemoryPermissions, + ) -> Result<(), &'static str> { + if virt.as_u64() & 0x1F_FFFF != 0 || phys.as_u64() & 0x1F_FFFF != 0 { + return Err("2MB block mapping requires 2MB alignment"); + } + + let pte_flags = perms.as_pte_flags() | flags::ATTR_NORMAL; + + let l0_phys = match ttbr { + Ttbr::Ttbr0 => self.ttbr0_l0, + Ttbr::Ttbr1 => self.ttbr1_l0, + }; + + let va = virt.as_u64(); + let l0_idx = ((va >> 39) & 0x1FF) as usize; + let l1_idx = ((va >> 30) & 0x1FF) as usize; + let l2_idx = ((va >> 21) & 0x1FF) as usize; + + let l1_phys = self.ensure_table(l0_phys, l0_idx)?; + let l2_phys = self.ensure_table(l1_phys, l1_idx)?; + + let l2_table = unsafe { &mut *(l2_phys.as_mut_ptr::()) }; + l2_table.entries[l2_idx] = phys.as_u64() | flags::VALID | flags::BLOCK | pte_flags; + + Ok(()) + } + + fn ensure_table( + &mut self, + table_phys: PhysAddr, + index: usize, + ) -> Result { + let table = unsafe { &mut *(table_phys.as_mut_ptr::()) }; + let entry = table.entries[index]; + + if entry & flags::VALID != 0 { + Ok(PhysAddr::new(entry & 0x0000_FFFF_FFFF_F000)) + } else { + let new_table = self + .allocator + .alloc_pages(1) + .ok_or("Failed to allocate page table")?; + + unsafe { + ptr::write_bytes(new_table.as_mut_ptr::(), 0, 4096); + } + + table.entries[index] = new_table.as_u64() | flags::VALID | flags::TABLE; + Ok(new_table) + } + } + + pub fn ttbr0(&self) -> u64 { + self.ttbr0_l0.as_u64() + } + + pub fn ttbr1(&self) -> u64 { + self.ttbr1_l0.as_u64() + } +} + +/// Create identity mapping for init_thread +pub fn create_identity_mapping( + setup: &mut MmuSetup, + start: PhysAddr, + end: PhysAddr, +) -> Result<(), &'static str> { + let start_aligned = start.align_down(2 * 1024 * 1024); + let end_aligned = end.align_up(2 * 1024 * 1024); + + let perms = MemoryPermissions { + readable: true, + writable: true, + executable: true, + }; // for init code -- FIXME: not necessarily! + + let mut addr = start_aligned; + while addr.as_u64() < end_aligned.as_u64() { + setup.map_block_2mb(Ttbr::Ttbr0, VirtAddr::new(addr.as_u64()), addr, perms)?; + addr = PhysAddr::new(addr.as_u64() + 2 * 1024 * 1024); + } + + Ok(()) +} + +/// Create higher-half mapping for kernel with proper per-section permissions +pub fn create_kernel_mapping( + setup: &mut MmuSetup, + layout: &KernelLayout, +) -> Result<(), &'static str> { + // Map each section with its specific permissions + for section in layout.iter_sections() { + map_section(setup, §ion)?; + } + + // Map BSS section + if let Some(bss) = layout.bss_mapping() { + map_section(setup, &bss)?; + } + + Ok(()) +} + +/// Map a single section with proper permissions +fn map_section(setup: &mut MmuSetup, section: &SectionMapping) -> Result<(), &'static str> { + // Check if we can use 2MB blocks (section must be 2MB aligned and sized) + let can_use_2mb = section.phys_start.is_aligned(2 * 1024 * 1024) + && section.virt_start.as_u64() % (2 * 1024 * 1024) == 0 + && section.size >= 2 * 1024 * 1024; + + if can_use_2mb { + // Use 2MB blocks for large aligned sections + let mut phys = section.phys_start; + let mut virt = section.virt_start; + let mut remaining = section.size; + + while remaining >= 2 * 1024 * 1024 { + setup.map_block_2mb(Ttbr::Ttbr1, virt, phys, section.permissions)?; + phys = PhysAddr::new(phys.as_u64() + 2 * 1024 * 1024); + virt = VirtAddr::new(virt.as_u64() + 2 * 1024 * 1024); + remaining -= 2 * 1024 * 1024; + } + + // Map remaining pages + while remaining > 0 { + setup.map_page(Ttbr::Ttbr1, virt, phys, section.permissions)?; + phys = PhysAddr::new(phys.as_u64() + 0x1000); + virt = VirtAddr::new(virt.as_u64() + 0x1000); + remaining = remaining.saturating_sub(0x1000); + } + } else { + // Use 4KB pages + for (phys, virt) in section.pages() { + setup.map_page(Ttbr::Ttbr1, virt, phys, section.permissions)?; + } + } + + Ok(()) +} + +/// Create device memory mapping +pub fn create_device_mapping( + setup: &mut MmuSetup, + phys: PhysAddr, + virt: VirtAddr, + size: usize, +) -> Result<(), &'static str> { + let pages = (size + 0xFFF) / 0x1000; + let perms = MemoryPermissions { + readable: true, + writable: true, + executable: false, + }; + + for i in 0..pages { + let offset = (i * 0x1000) as u64; + + let _l0_phys = setup.ttbr1_l0; + let va = virt.as_u64() + offset; + let pa = phys.as_u64() + offset; + + // Use device memory attributes + let pte_flags = perms.as_pte_flags() | flags::ATTR_DEVICE; + setup.map_page_with_flags(Ttbr::Ttbr1, VirtAddr::new(va), PhysAddr::new(pa), pte_flags)?; + } + + Ok(()) +} diff --git a/kernel/nucleus/nucleus.ld b/kernel/nucleus/nucleus.ld index 9c8660625..c280b2eb9 100644 --- a/kernel/nucleus/nucleus.ld +++ b/kernel/nucleus/nucleus.ld @@ -1,6 +1,6 @@ -/* kernel/kernel.ld - Higher-half kernel with vector table */ +/* Higher-half kernel with vector table */ -ENTRY(_kernel_entry) +/* ENTRY(_kernel_entry) */ KERNEL_VIRT_BASE = 0xFFFF000000000000; diff --git a/kernel/nucleus/src/main.rs b/kernel/nucleus/src/main.rs index 928b6f52d..99e8d525f 100644 --- a/kernel/nucleus/src/main.rs +++ b/kernel/nucleus/src/main.rs @@ -34,237 +34,11 @@ use { // , time }; -libboot::entry!(init_thread); +// kernel/src/main.rs - Kernel entry points are exception handlers in mod vectors -macro_rules! early_print { - // early_print!("a {} event", "log") - ($($arg:tt)+) => { - let mut buf = [0_u8; 4096]; // Increase this buffer size to allow dumping larger panic texts. - libqemu::semihosting::sys_write0_call( - libconsole::write_to::c_show(&mut buf, format_args!($($arg)+)).unwrap(), - ); - } -} - -/// Kernel early init code. -/// `arch` crate is responsible for calling it. -/// -/// # Safety -/// -/// - Only a single core must be active and running this function. -/// - The init calls in this function must appear in the correct order: -/// - MMU + Data caching must be activated at the earliest. Without it, any atomic operations, -/// e.g. the yet-to-be-introduced spinlocks in the device drivers (which currently employ -/// `IRQSafeNullLocks` instead of spinlocks), will fail to work (properly) on the `RPi` `SoCs`. -pub unsafe fn init_thread() -> ! { - // Entered in EL2: - - early_print!("Entered EL2"); - - early_print!("DTB at"); - early_print!("Max RAM size is"); - early_print!("Init thread image covers phys -:- identity mapped"); - early_print!("Init thread mapping tables filled in as - entries"); - early_print!("Kernel image covers phys -:- mapped to KERNEL_HIGH_BASE:-"); - early_print!("Kernel mapping tables filled in as - for kernel, as - for phys memory"); - - // #[cfg(feature = "jtag")] - // libmachine::debug::jtag::wait_debugger(); - - // libexception::exception::handling_init(); - - // SAFETY: Not safe! - let phys_kernel_tables_base_addr = match unsafe { libmemory::mmu::kernel_map_binary() } { - Err(string) => panic!("Error mapping kernel binary: {}", string), - Ok(addr) => addr, - }; - - // SAFETY: Not safe! - if let Err(e) = unsafe { libmemory::mmu::enable_mmu_and_caching(phys_kernel_tables_base_addr) } - { - panic!("Enabling MMU failed: {}", e); - } - - libmemory::mmu::post_enable_init(); - - // After page tables are populated and MMU is on, switch to EL1, now kernel will already be higher-half mapped. - - // SAFETY: Not safe! - // if let Err(x) = unsafe { libplatform::platform::drivers::init() } { - // panic!("Error initializing platform drivers: {}", x); - // } - - // Initialize all device drivers. - // SAFETY: Not safe! - // unsafe { - // libplatform::platform::drivers::driver_manager().init_drivers_and_irqs(); - // } - - // Unmask interrupts on the boot CPU core. - // libexception::exception::asynchronous::local_irq_unmask(); - - // Announce conclusion of the kernel_init() phase. - // libkernel_state::state_manager().transition_to_single_core_main(); - - // libconsole::init_logger(); - - // info!("{}", libkernel::version()); - // info!("Booting on: {}", bsp::board_name()); - - info!( - "{} version {}", - env!("CARGO_PKG_NAME"), - env!("CARGO_PKG_VERSION") - ); - info!( - "Booting on: {}", - libplatform::platform::BcmHost::board_name() - ); - - // info!("MMU online. Special regions:"); - // machine::platform::memory::mmu::virt_mem_layout().print_layout(); - - dump_memory_map(); - - let (_, privilege_level) = libexception::exception::current_privilege_level(); - info!("Current privilege level: {privilege_level}"); - - info!("Exception handling state:"); - libexception::exception::asynchronous::print_state(); - - info!( - "Architectural timer resolution: {} ns", - libtime::time::time_manager().resolution().as_nanos() - ); - - info!("Drivers loaded:"); - libplatform::platform::drivers::driver_manager().enumerate(); - - info!("Registered IRQ handlers:"); - libplatform::platform::exception::asynchronous::irq_manager().print_handler(); - - // Test a failing timer case. - libtime::time::time_manager().spin_for(Duration::from_nanos(1)); - - for _ in 0..3 { - info!("Spinning for 1 second"); - libtime::time::time_manager().spin_for(Duration::from_secs(1)); - } - - command_prompt(); - - reboot() -} +mod vectors; #[panic_handler] fn panicked(info: &PanicInfo) -> ! { libmachine::panic::handler(info) } - -fn print_mmu_state_and_features() { - libmemory::arch::features::print_features(); -} - -fn dump_memory_map() { - // Output the memory map as we could derive from FDT and information about our loaded image - // Use it to imagine how the memmap would look like in the end. - // arch::memory::print_layout(); -} - -//------------------------------------------------------------ -// Start a command prompt -//------------------------------------------------------------ -fn command_prompt() { - 'cmd_loop: loop { - let mut buf = [0_u8; 64]; - - match libconsole::console::command_prompt(&mut buf) { - // b"mmu" => init_mmu(), - b"feats" => print_mmu_state_and_features(), - // b"disp" => check_display_init(), - // b"trap" => check_data_abort_trap(), - // b"map" => machine::platform::memory::mmu::virt_mem_layout().print_layout(), - // b"led on" => set_led(true), - // b"led off" => set_led(false), - b"help" => print_help(), - b"end" => break 'cmd_loop, - x => warn!("[!] Unknown command {x:?}, try 'help'"), - } - } -} - -fn print_help() { - println!("Supported console commands:"); - println!(" mmu - initialize MMU"); - println!(" feats - print MMU state and supported features"); - #[cfg(not(feature = "noserial"))] - println!(" uart - try to reinitialize UART serial"); - // println!(" disp - try to init VC framebuffer and draw some text"); - println!(" trap - trigger and recover from a data abort exception"); - println!(" map - show kernel memory layout"); - // println!(" led [on|off] - change RPi LED status"); - println!(" end - leave console and reset board"); -} - -// fn set_led(enable: bool) { -// let mut mbox = Mailbox::<8>::default(); -// let index = mbox.request(); -// let index = mbox.set_led_on(index, enable); -// let mbox = mbox.end(index); -// -// mbox.call(channel::PropertyTagsArmToVc) -// .map_err(|e| { -// warn!("Mailbox call returned error {}", e); -// warn!("Mailbox contents: {:?}", mbox); -// }) -// .ok(); -// } - -fn reboot() -> ! { - cfg_if! { - if #[cfg(feature = "qemu")] { - info!("Bye, shutting down QEMU"); - libqemu::semihosting::exit_success() - } else { - // use machine::platform::raspberrypi::power::Power; - - info!("Bye, going to reset now"); - // Power::default().reset() - libcpu::endless_sleep() - } - } -} - -// fn check_display_init() { -// display_graphics() -// .map_err(|e| { -// warn!("Error in display: {}", e); -// }) -// .ok(); -// } -// -// fn display_graphics() -> Result<(), DrawError> { -// if let Ok(mut display) = VC::init_fb(800, 600, 32) { -// info!("Display created"); -// -// display.clear(Color::black()); -// info!("Display cleared"); -// -// display.rect(10, 10, 250, 250, Color::rgb(32, 96, 64)); -// display.draw_text(50, 50, "Hello there!", Color::rgb(128, 192, 255))?; -// -// let mut buf = [0u8; 64]; -// let s = machine::write_to::show(&mut buf, format_args!("Display width {}", display.width)); -// -// if s.is_err() { -// display.draw_text(50, 150, "Error displaying", Color::red())? -// } else { -// display.draw_text(50, 150, s.unwrap(), Color::white())? -// } -// -// display.draw_text(150, 50, "RED", Color::red())?; -// display.draw_text(160, 60, "GREEN", Color::green())?; -// display.draw_text(170, 70, "BLUE", Color::blue())?; -// } -// Ok(()) -// } diff --git a/kernel/nucleus/src/vectors.rs b/kernel/nucleus/src/vectors.rs new file mode 100644 index 000000000..2ae403124 --- /dev/null +++ b/kernel/nucleus/src/vectors.rs @@ -0,0 +1,125 @@ +// kernel/src/vectors.rs - Example exception vector table for kernel + +use core::arch::global_asm; + +// Exception vector table must be 2KB aligned +// 16 entries × 128 bytes each = 2048 bytes total +// +// The table is organized as: +// - 4 entries for exceptions from current EL with SP_EL0 +// - 4 entries for exceptions from current EL with SP_ELx +// - 4 entries for exceptions from lower EL (AArch64) +// - 4 entries for exceptions from lower EL (AArch32) + +global_asm!( + r#" +.section .vectors, "ax" +.balign 2048 +.global __vectors + +__vectors: + // ═══════════════════════════════════════════════════════════════ + // Current EL with SP_EL0 (not used, we use SP_EL1) + // ═══════════════════════════════════════════════════════════════ + +.balign 128 +curr_el_sp0_sync: + b exception_handler_sync + +.balign 128 +curr_el_sp0_irq: + b exception_handler_irq + +.balign 128 +curr_el_sp0_fiq: + b exception_handler_fiq + +.balign 128 +curr_el_sp0_serror: + b exception_handler_serror + + // ═══════════════════════════════════════════════════════════════ + // Current EL with SP_ELx (kernel exceptions) + // ═══════════════════════════════════════════════════════════════ + +.balign 128 +curr_el_spx_sync: + b exception_handler_sync + +.balign 128 +curr_el_spx_irq: + b exception_handler_irq + +.balign 128 +curr_el_spx_fiq: + b exception_handler_fiq + +.balign 128 +curr_el_spx_serror: + b exception_handler_serror + + // ═══════════════════════════════════════════════════════════════ + // Lower EL using AArch64 (user-space syscalls and exceptions) + // ═══════════════════════════════════════════════════════════════ + +.balign 128 +lower_el_aarch64_sync: + b syscall_handler // SVC from user space + +.balign 128 +lower_el_aarch64_irq: + b exception_handler_irq + +.balign 128 +lower_el_aarch64_fiq: + b exception_handler_fiq + +.balign 128 +lower_el_aarch64_serror: + b exception_handler_serror + + // ═══════════════════════════════════════════════════════════════ + // Lower EL using AArch32 (not supported) + // ═══════════════════════════════════════════════════════════════ + +.balign 128 +lower_el_aarch32_sync: + b exception_handler_unsupported + +.balign 128 +lower_el_aarch32_irq: + b exception_handler_unsupported + +.balign 128 +lower_el_aarch32_fiq: + b exception_handler_unsupported + +.balign 128 +lower_el_aarch32_serror: + b exception_handler_unsupported + +// ═══════════════════════════════════════════════════════════════ +// Exception handlers (stubs - implement properly in Rust) +// ═══════════════════════════════════════════════════════════════ + +exception_handler_sync: + // Save context, call Rust handler, restore context + b . + +exception_handler_irq: + b . + +exception_handler_fiq: + b . + +exception_handler_serror: + b . + +exception_handler_unsupported: + b . + +syscall_handler: + // This is where CapInvoke syscalls arrive + b . +"# +); From ac1cb89ce1a9799808d0c8ae7f8a35318c708e9e Mon Sep 17 00:00:00 2001 From: Berkus Decker Date: Sat, 24 Jan 2026 00:33:34 +0200 Subject: [PATCH 012/107] wip: split nucleus + init_thread = kernel --- Cargo.toml | 9 +- kernel/build.rs | 10 --- kernel/init_thread/Cargo.toml | 67 ++++++++++++-- kernel/init_thread/build.rs | 14 +-- kernel/init_thread/src/memory.rs | 3 + kernel/nucleus/Cargo.toml | 3 + kernel/nucleus/build.rs | 17 +++- kernel/nucleus/nucleus.ld | 144 +++++++++++++++++++++++-------- kernel/nucleus/src/main.rs | 2 - kernel/nucleus/src/vectors.rs | 2 +- 10 files changed, 206 insertions(+), 65 deletions(-) delete mode 100644 kernel/build.rs diff --git a/Cargo.toml b/Cargo.toml index c87bfc686..bca16d11e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,7 @@ [workspace] members = [ - "kernel", + "kernel/nucleus", + "kernel/init_thread", "bin/chainboot", "bin/chainofcommand", # Libraries @@ -81,6 +82,12 @@ tokio = { version = "1.49", features = ["full"] } tokio-serial = { version = "5.4" } tokio-stream = { version = "0.1" } tokio-util = { version = "0.7", features = ["codec", "io"] } +#========== +# build.rs +#========== +goblin = { version = "0.10" } +build-rs = { version = "0.3" } +build-print = { version = "1.0" } #======= # mixed #======= diff --git a/kernel/build.rs b/kernel/build.rs deleted file mode 100644 index b3aafde7a..000000000 --- a/kernel/build.rs +++ /dev/null @@ -1,10 +0,0 @@ -//! This build script is used to link main kernel binary. - -const LINKER_SCRIPT: &str = "libs/platform/src/platform/raspberrypi/linker/kernel.ld"; -const LINKER_SCRIPT_AUX: &str = "libs/exception/src/arch/aarch64/linker/aarch64-exceptions.ld"; - -fn main() { - println!("cargo:rerun-if-env-changed=TARGET_BOARD"); - println!("cargo:rerun-if-changed={LINKER_SCRIPT}"); - println!("cargo:rerun-if-changed={LINKER_SCRIPT_AUX}"); -} diff --git a/kernel/init_thread/Cargo.toml b/kernel/init_thread/Cargo.toml index f577f1c00..09ecaa6f3 100644 --- a/kernel/init_thread/Cargo.toml +++ b/kernel/init_thread/Cargo.toml @@ -1,11 +1,68 @@ [package] name = "init_thread" -version = "0.1.0" -edition = "2024" +description = "Boot time initialization" +authors = { workspace = true } +categories = { workspace = true } +documentation = { workspace = true } +edition = { workspace = true } +homepage = { workspace = true } +license = { workspace = true } +publish = { workspace = true } +readme = { workspace = true } +repository = { workspace = true } +version = { workspace = true } + +[features] +default = [] +# Enable JTAG debugging of kernel - enable jtag helpers and +# block waiting for JTAG probe attach at the start of kernel main. +jtag = ["libmachine/jtag"] +noserial = ["libplatform/noserial"] +# Build for running under QEMU with semihosting, so various halt/reboot options would for example quit QEMU instead. +qemu = [ + "dep:libqemu", + "libconsole/qemu", + "libexception/qemu", + "libmachine/qemu", + "libplatform/qemu", +] [dependencies] +aarch64-cpu = { workspace = true } +bit_field = { workspace = true } +bitflags = { workspace = true } +cfg-if = { workspace = true } +libboot = { workspace = true } +libconsole = { workspace = true } +libcpu = { workspace = true } +libexception = { workspace = true } +libkernel-state = { workspace = true } +liblog = { workspace = true } +libmachine = { workspace = true } +libmemory = { workspace = true } +libplatform = { workspace = true } +libqemu = { workspace = true, optional = true } +libtime = { workspace = true } +snafu = { workspace = true } +static_assertions = { workspace = true } +tock-registers = { workspace = true } +usize_conversions = { workspace = true } +ux = { workspace = true } + +[dev-dependencies] +libexception = { workspace = true, features = ["test_build"] } +liblocking = { workspace = true } +libqemu = { workspace = true } +libtest = { workspace = true } [build-dependencies] -goblin = "0.10" -build-rs = "0.3" -build-print = "1.0" +goblin = { workspace = true } +build-rs = { workspace = true } +build-print = { workspace = true } + +[[bin]] +name = "init_thread" +test = false # test are all concentrated in integration tests/ + +[lints] +workspace = true diff --git a/kernel/init_thread/build.rs b/kernel/init_thread/build.rs index e46be17eb..96bbeafd7 100644 --- a/kernel/init_thread/build.rs +++ b/kernel/init_thread/build.rs @@ -13,13 +13,13 @@ use { }; fn main() { - let kernel_elf_path = "../target/aarch64-unknown-none/release/kernel"; // must be passed-in as input? + let kernel_elf_path = "../../target/aarch64-metta-none-eabi/release/nucleus"; // must be passed-in as input? output::rerun_if_changed(kernel_elf_path); output::rerun_if_changed("build.rs"); - output::rustc_link_arg( - format!("--script={}/init_thread.ld", env!("CARGO_MANIFEST_DIR")).as_ref(), - ); + // output::rustc_link_arg( + // format!("--script={}/init_thread.ld", env!("CARGO_MANIFEST_DIR")).as_ref(), + // ); let out_dir = env::var("OUT_DIR").unwrap(); let out_path = Path::new(&out_dir); @@ -254,7 +254,7 @@ fn generate_rust_code(sections: &KernelSections, _elf: &Elf, out_path: &Path) { #[allow(unused)] use crate::{ - loader::{BssSectionMeta, KernelImageInfo, KernelSectionMeta, LoadableSection, VectorTableMeta}, + loader::{KernelImageInfo, KernelSectionMeta, LoadableSection, VectorTableMeta}, memory::MemoryPermissions }; @@ -316,10 +316,12 @@ use crate::{ // BSS metadata if let Some(ref bss) = sections.bss_section { code.push_str(&format!( - r#"pub const BSS_META: BssSectionMeta = BssSectionMeta {{ + r#"pub const BSS_META: KernelSectionMeta = KernelSectionMeta {{ + name: ".bss", virt_addr: 0x{:016X}, size: 0x{:X}, alignment: 0x{:X}, + permissions: MemoryPermissions {{ readable: true, writable: true, executable: false }}, }}; "#, diff --git a/kernel/init_thread/src/memory.rs b/kernel/init_thread/src/memory.rs index 531064de9..fe0110298 100644 --- a/kernel/init_thread/src/memory.rs +++ b/kernel/init_thread/src/memory.rs @@ -53,6 +53,9 @@ impl VirtAddr { pub const fn is_higher_half(self) -> bool { self.0 >= 0xFFFF_0000_0000_0000 } + pub const fn is_aligned(self, align: u64) -> bool { + self.0 & (align - 1) == 0 + } } pub struct BootAllocator { diff --git a/kernel/nucleus/Cargo.toml b/kernel/nucleus/Cargo.toml index b46761366..5985e33ee 100644 --- a/kernel/nucleus/Cargo.toml +++ b/kernel/nucleus/Cargo.toml @@ -58,6 +58,9 @@ liblocking = { workspace = true } libqemu = { workspace = true } libtest = { workspace = true } +[build-dependencies] +build-rs = { workspace = true } + [[bin]] name = "nucleus" test = false # test are all concentrated in integration tests/ diff --git a/kernel/nucleus/build.rs b/kernel/nucleus/build.rs index aeed8650e..c3db5d2f7 100644 --- a/kernel/nucleus/build.rs +++ b/kernel/nucleus/build.rs @@ -1,5 +1,16 @@ +//! This build script is used to link main kernel binary. + fn main() { - build_rs::output::rustc_link_arg( - format!("--script={}/kernel.ld", env!("CARGO_MANIFEST_DIR")).as_ref(), - ); + // build_rs::output::rustc_link_arg( + // format!("--script={}/nucleus.ld", env!("CARGO_MANIFEST_DIR")).as_ref(), + // ); } + +// const LINKER_SCRIPT: &str = "libs/platform/src/platform/raspberrypi/linker/kernel.ld"; +// const LINKER_SCRIPT_AUX: &str = "libs/exception/src/arch/aarch64/linker/aarch64-exceptions.ld"; + +// fn main() { +// println!("cargo:rerun-if-env-changed=TARGET_BOARD"); +// println!("cargo:rerun-if-changed={LINKER_SCRIPT}"); +// println!("cargo:rerun-if-changed={LINKER_SCRIPT_AUX}"); +// } diff --git a/kernel/nucleus/nucleus.ld b/kernel/nucleus/nucleus.ld index c280b2eb9..9ec87069c 100644 --- a/kernel/nucleus/nucleus.ld +++ b/kernel/nucleus/nucleus.ld @@ -1,59 +1,129 @@ -/* Higher-half kernel with vector table */ +/* + * SPDX-License-Identifier: MIT OR BlueOak-1.0.0 + * Copyright (c) 2018 Andre Richter + * Copyright (c) Berkus Decker + * Original code distributed under MIT, additional changes are under BlueOak-1.0.0 + */ -/* ENTRY(_kernel_entry) */ +__PAGE_SIZE = 64K; +__PAGE_MASK = __PAGE_SIZE - 1; -KERNEL_VIRT_BASE = 0xFFFF000000000000; +__KERNEL_VIRT_ADDR_BASE = 0xffff000000000000; /* Nucleus is mapped here, init_thread is identity-mapped */ + +/* Flags: + * 4 == R + * 5 == RX + * 6 == RW + * + * Segments are marked PT_LOAD below so that the ELF file provides virtual and physical addresses. + * It doesn't mean all of them need actually be loaded. + */ +PHDRS +{ + segment_code PT_LOAD FLAGS(5); + segment_data PT_LOAD FLAGS(6); +} SECTIONS { - . = KERNEL_VIRT_BASE; + . = __KERNEL_VIRT_ADDR_BASE; - __kernel_start = .; + /* __nucleus_start = .; */ - /* Code section */ - .text : AT(ADDR(.text) - KERNEL_VIRT_BASE) ALIGN(4K) + .text : /*AT(ADDR(.text) - __KERNEL_VIRT_ADDR_BASE)*/ ALIGN(__PAGE_SIZE) { - __text_start = .; - *(.text.entry) - *(.text .text.*) - . = ALIGN(2K); /* Ensure full 2KB for vector table */ + /******************************************************************************************* + * Regular Kernel Code + *******************************************************************************************/ + *(.text*) + . = ALIGN(2K); KEEP(*(.vectors)) - . = ALIGN(2K); /* Ensure full 2KB for vector table */ - __text_end = .; - } + . = ALIGN(2K); + } :segment_code - /* Read-only data */ - .rodata : AT(ADDR(.rodata) - KERNEL_VIRT_BASE) ALIGN(4K) + /* TODO: rodata is currently executable, but doesn't have to... */ + .rodata : /*AT(ADDR(.rodata) - __KERNEL_VIRT_ADDR_BASE)*/ ALIGN(4) { - __rodata_start = .; - *(.rodata .rodata.*) - __rodata_end = .; - } + *(.rodata*) + FILL(0x00) + . = ALIGN(__PAGE_SIZE); /* Fill up to page size */ + } :segment_code + + /* __CODE_END = .; + ASSERT((__CODE_END & __PAGE_MASK) == 0, "End of kernel code is not page aligned") */ - /* Initialized data */ - .data : AT(ADDR(.data) - KERNEL_VIRT_BASE) ALIGN(4K) + /*********************************************************************************************** + * Data + BSS + ***********************************************************************************************/ + + .data : /*AT(ADDR(.data) - __KERNEL_VIRT_ADDR_BASE)*/ ALIGN(__PAGE_SIZE) { - __data_start = .; - *(.data .data.*) - __data_end = .; - } + /* __DATA_START = .; + ASSERT((__DATA_START & __PAGE_MASK) == 0, "Start of kernel data is not page aligned") */ + + *(.data*) + FILL(0x00) - /* BSS */ - .bss : AT(ADDR(.bss) - KERNEL_VIRT_BASE) ALIGN(4K) + . = ALIGN(__PAGE_SIZE); + } :segment_data + + .bss (NOLOAD) : /*AT(ADDR(.bss) - __KERNEL_VIRT_ADDR_BASE)*/ ALIGN(__PAGE_SIZE) { - __bss_start = .; - *(.bss .bss.*) + /* __BSS_START = LOADADDR(.bss); LMA (PhysAddr) - for the loader in init_thread */ + *(.bss*) *(COMMON) - __bss_end = .; - } + . = ALIGN(__PAGE_SIZE); /* Align up to page size */ + } :segment_data - . = ALIGN(4K); - __kernel_end = .; + /* __BSS_END = __BSS_START + ALIGN(SIZEOF(.bss), __PAGE_SIZE); *//* LMA, Physical address */ + /* __DATA_END = .; *//* VMA, Virtual address */ - /DISCARD/ : - { - *(.comment) + /*********************************************************************************************** + * MMIO Remap Reserved (8Mb) + ***********************************************************************************************/ + /* __MMIO_REMAP_START = .; FIXME: VMA? Must be LMA perhaps, the point is to remap this + . += 8 * 1024 * 1024; + __MMIO_REMAP_END = .; FIXME: VMA? Must be LMA perhaps, the point is to remap this */ + + /* ASSERT((. & __PAGE_MASK) == 0, "MMIO remap reservation is not page aligned") + __kernel_virt_addr_space_size = ABSOLUTE(. - __KERNEL_VIRT_ADDR_BASE); */ + + /* Physical end */ + /* __nucleus_end = __nucleus_start + __kernel_virt_addr_space_size; + __kernel_end = .; */ + + /*********************************************************************************************** + * Misc + ***********************************************************************************************/ + + .got : { *(.got*) } + ASSERT(SIZEOF(.got) == 0, "Unexpected relocations in kernel binary.") + + /DISCARD/ : { + *(.comment*) + *(.gnu*) *(.note*) *(.eh_frame*) + *(.text.chainboot*) } } + +PROVIDE(current_el0_synchronous = current_el0_synchronous); +PROVIDE(current_el0_irq = current_el0_irq); +PROVIDE(current_el0_fiq = default_exception_handler); +PROVIDE(current_el0_serror = current_el0_serror); + +PROVIDE(current_elx_synchronous = current_elx_synchronous); +PROVIDE(current_elx_irq = current_elx_irq); +PROVIDE(current_elx_fiq = default_exception_handler); +PROVIDE(current_elx_serror = current_elx_serror); + +PROVIDE(lower_aarch64_synchronous = lower_aarch64_synchronous); +PROVIDE(lower_aarch64_irq = lower_aarch64_irq); +PROVIDE(lower_aarch64_fiq = default_exception_handler); +PROVIDE(lower_aarch64_serror = lower_aarch64_serror); + +PROVIDE(lower_aarch32_synchronous = lower_aarch32_synchronous); +PROVIDE(lower_aarch32_irq = lower_aarch32_irq); +PROVIDE(lower_aarch32_fiq = default_exception_handler); +PROVIDE(lower_aarch32_serror = lower_aarch32_serror); diff --git a/kernel/nucleus/src/main.rs b/kernel/nucleus/src/main.rs index 99e8d525f..137b664e4 100644 --- a/kernel/nucleus/src/main.rs +++ b/kernel/nucleus/src/main.rs @@ -34,8 +34,6 @@ use { // , time }; -// kernel/src/main.rs - Kernel entry points are exception handlers in mod vectors - mod vectors; #[panic_handler] diff --git a/kernel/nucleus/src/vectors.rs b/kernel/nucleus/src/vectors.rs index 2ae403124..cb4e71ad5 100644 --- a/kernel/nucleus/src/vectors.rs +++ b/kernel/nucleus/src/vectors.rs @@ -1,4 +1,4 @@ -// kernel/src/vectors.rs - Example exception vector table for kernel +// Exception vector table for kernel use core::arch::global_asm; From d55fcc43d5ce19d027584dc3f9e379ece505ffe8 Mon Sep 17 00:00:00 2001 From: Berkus Decker Date: Sun, 22 Feb 2026 04:51:29 +0200 Subject: [PATCH 013/107] wip: libmemory --- libs/memory/Cargo.toml | 2 +- libs/memory/src/arch/aarch64/mmu/mod.rs | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/libs/memory/Cargo.toml b/libs/memory/Cargo.toml index f3c6e062a..4b5bf3ab6 100644 --- a/libs/memory/Cargo.toml +++ b/libs/memory/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "libmemory" -description = "Vesper nanokernel Memory Descriptors shared code library, useful also for the ttt tool." +description = "Vesper nanokernel Memory Descriptors shared code library." authors = { workspace = true } categories = { workspace = true } diff --git a/libs/memory/src/arch/aarch64/mmu/mod.rs b/libs/memory/src/arch/aarch64/mmu/mod.rs index 529f6fb57..10d5cfeb0 100644 --- a/libs/memory/src/arch/aarch64/mmu/mod.rs +++ b/libs/memory/src/arch/aarch64/mmu/mod.rs @@ -180,7 +180,6 @@ impl interface::MMU for MemoryManagementUnit { // TTBR0_EL1.set_baddr(KERNEL_TABLES.entries.base_addr_u64()); // User (lo-)space addresses // TTBR0_EL1.modify(TTBR0_EL1::CnP.val(1)); - // TODO: also do kernel level tables (same mappings but at higher table addresses? need to update ttt to do it) // TTBR1_EL1.set_baddr(LVL1_TABLE.entries.base_addr_u64()); // Kernel (hi-)space addresses // TTBR1_EL1.modify(TTBR1_EL1::CnP.val(1)); From 4653d47571f15cd8691f764143dea1531e0ae000 Mon Sep 17 00:00:00 2001 From: Berkus Decker Date: Sat, 24 Jan 2026 15:09:55 +0200 Subject: [PATCH 014/107] wip: generate kernel meta from template --- Cargo.toml | 1 + kernel/init_thread/Cargo.toml | 1 + kernel/init_thread/build.rs | 233 ++++++------------ .../init_thread/kernel_sections.template.rs | 68 +++++ kernel/init_thread/src/loader.rs | 92 +++---- kernel/init_thread/src/main.rs | 4 +- kernel/init_thread/src/memory.rs | 20 +- kernel/nucleus/{build.rs => _build.rs} | 0 libs/memory/src/arch/aarch64/virt_page.rs | 2 +- libs/memory/src/lib.rs | 1 + 10 files changed, 192 insertions(+), 230 deletions(-) create mode 100644 kernel/init_thread/kernel_sections.template.rs rename kernel/nucleus/{build.rs => _build.rs} (100%) diff --git a/Cargo.toml b/Cargo.toml index bca16d11e..e665fbfa9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -88,6 +88,7 @@ tokio-util = { version = "0.7", features = ["codec", "io"] } goblin = { version = "0.10" } build-rs = { version = "0.3" } build-print = { version = "1.0" } +minijinja = { version = "2", features = ["loader"] } #======= # mixed #======= diff --git a/kernel/init_thread/Cargo.toml b/kernel/init_thread/Cargo.toml index 09ecaa6f3..cd9c51351 100644 --- a/kernel/init_thread/Cargo.toml +++ b/kernel/init_thread/Cargo.toml @@ -59,6 +59,7 @@ libtest = { workspace = true } goblin = { workspace = true } build-rs = { workspace = true } build-print = { workspace = true } +minijinja = { workspace = true } [[bin]] name = "init_thread" diff --git a/kernel/init_thread/build.rs b/kernel/init_thread/build.rs index 96bbeafd7..cc4f795cc 100644 --- a/kernel/init_thread/build.rs +++ b/kernel/init_thread/build.rs @@ -17,6 +17,7 @@ fn main() { output::rerun_if_changed(kernel_elf_path); output::rerun_if_changed("build.rs"); + output::rerun_if_changed("kernel_sections.template.rs"); // output::rustc_link_arg( // format!("--script={}/init_thread.ld", env!("CARGO_MANIFEST_DIR")).as_ref(), // ); @@ -46,7 +47,7 @@ fn main() { let sections = extract_sections(&elf, &elf_bytes, out_path); // Generate Rust code - generate_rust_code(§ions, &elf, out_path); + generate_rust_code(§ions, out_path); } /// Extracted section with all metadata needed for loading @@ -58,7 +59,7 @@ struct ExtractedSection { /// Size in memory mem_size: usize, /// Size in file (0 for BSS) - file_size: usize, + // file_size: usize, /// Required alignment alignment: u64, /// Permissions @@ -66,18 +67,31 @@ struct ExtractedSection { writable: bool, executable: bool, /// Is this a NOBITS section (BSS)? - is_nobits: bool, + // is_nobits: bool, /// Output binary file name (None for BSS) bin_file: Option, } +impl ExtractedSection { + fn meta_name(&self) -> String { + format!("{}_META", self.name.trim_start_matches('.').to_uppercase()) + } + + fn bin_name(&self) -> String { + format!( + "KERNEL_{}_BIN", + self.name.trim_start_matches('.').to_uppercase() + ) + } +} + /// Exception vector table information #[derive(Debug)] struct VectorTableInfo { /// Virtual address of the vector table virt_addr: u64, /// Size of the vector table (should be 0x800 = 2048 bytes) - size: usize, + mem_size: usize, /// Alignment requirement (must be 2KB aligned for VBAR) alignment: u64, } @@ -98,7 +112,6 @@ struct KernelSections { fn extract_sections(elf: &Elf, elf_bytes: &[u8], out_path: &Path) -> KernelSections { let mut load_sections = Vec::new(); let mut bss_section = None; - let mut vector_table = None; let mut virt_base = u64::MAX; // First pass: find virtual base from program headers @@ -136,12 +149,12 @@ fn extract_sections(elf: &Elf, elf_bytes: &[u8], out_path: &Path) -> KernelSecti name: name.to_string(), virt_addr: sh.sh_addr, mem_size: sh.sh_size as usize, - file_size: if is_nobits { 0 } else { sh.sh_size as usize }, + // file_size: if is_nobits { 0 } else { sh.sh_size as usize }, alignment: sh.sh_addralign, readable, writable, executable, - is_nobits, + // is_nobits, bin_file: None, }; @@ -153,7 +166,7 @@ fn extract_sections(elf: &Elf, elf_bytes: &[u8], out_path: &Path) -> KernelSecti } // Try to find vectors via symbol - vector_table = find_vector_table_from_symbols(elf); + let vector_table = find_vector_table_from_symbols(elf); // Sort load sections by virtual address load_sections.sort_by_key(|s| s.virt_addr); @@ -229,7 +242,7 @@ fn find_vector_table_from_symbols(elf: &Elf) -> Option { return Some(VectorTableInfo { virt_addr: sym.st_value, // If size is 0, assume standard size of 0x800 - size: if sym.st_size > 0 { + mem_size: if sym.st_size > 0 { sym.st_size as usize } else { 0x800 @@ -244,166 +257,84 @@ fn find_vector_table_from_symbols(elf: &Elf) -> Option { None } -fn generate_rust_code(sections: &KernelSections, _elf: &Elf, out_path: &Path) { - let mut code = String::new(); - - // Header - code.push_str( - r#"// Auto-generated kernel section metadata and binary includes -// DO NOT EDIT - Generated by build.rs - -#[allow(unused)] -use crate::{ - loader::{KernelImageInfo, KernelSectionMeta, LoadableSection, VectorTableMeta}, - memory::MemoryPermissions -}; - -"#, - ); - - // Generate binary includes - code.push_str("// ═══════════════════════════════════════════════════════════════\n"); - code.push_str("// Binary section data (included at compile time)\n"); - code.push_str("// ═══════════════════════════════════════════════════════════════\n\n"); - - for section in §ions.load_sections { - let const_name = format!( - "KERNEL_{}_BIN", - section.name.trim_start_matches('.').to_uppercase() - ); - let bin_file = section.bin_file.as_ref().unwrap(); +fn generate_rust_code(sections: &KernelSections, out_path: &Path) { + use minijinja::{Environment, context}; + + let mut code = Environment::new(); + code.set_trim_blocks(true); + code.add_filter("address", |v: usize| format!("0x{v:016X}")); + code.add_filter("hex", |v: usize| format!("0x{v:X}")); + code.add_template_owned( + "sections", + std::fs::read_to_string("kernel_sections.template.rs").unwrap(), + ) + .unwrap(); + let tmpl = code.get_template("sections").unwrap(); + + let sections_tmpl: Vec<_> = sections + .load_sections + .iter() + .map(|section| { + context! { + name => section.name, + meta_name => section.meta_name(), + bin_name => section.bin_name(), + bin_file => section.bin_file, + virt_addr => section.virt_addr, + size => section.mem_size, + align => section.alignment, + r => section.readable, + w => section.writable, + x => section.executable, + } + }) + .collect(); - code.push_str(&format!( - "static {}: &[u8] = include_bytes!(concat!(env!(\"OUT_DIR\"), \"/{}\"));\n", - const_name, bin_file - )); + if sections.bss_section.is_none() { + output::error("No BSS section for kernel, that's a bummer!"); } - code.push('\n'); - - // Generate section metadata constants - code.push_str("// ═══════════════════════════════════════════════════════════════\n"); - code.push_str("// Section metadata\n"); - code.push_str("// ═══════════════════════════════════════════════════════════════\n\n"); - - for section in §ions.load_sections { - let const_name = format!( - "{}_META", - section.name.trim_start_matches('.').to_uppercase() - ); - - code.push_str(&format!( - r#"pub const {}: KernelSectionMeta = KernelSectionMeta {{ - name: "{}", - virt_addr: 0x{:016X}, - size: 0x{:X}, - alignment: 0x{:X}, - permissions: MemoryPermissions {{ readable: {}, writable: {}, executable: {} }}, -}}; - -"#, - const_name, - section.name, - section.virt_addr, - section.mem_size, - section.alignment, - section.readable, - section.writable, - section.executable, - )); - } + let bss_section = sections.bss_section.as_ref().unwrap(); - // BSS metadata - if let Some(ref bss) = sections.bss_section { - code.push_str(&format!( - r#"pub const BSS_META: KernelSectionMeta = KernelSectionMeta {{ - name: ".bss", - virt_addr: 0x{:016X}, - size: 0x{:X}, - alignment: 0x{:X}, - permissions: MemoryPermissions {{ readable: true, writable: true, executable: false }}, -}}; - -"#, - bss.virt_addr, bss.mem_size, bss.alignment, - )); - } + let bss_tmpl = context! { + virt_addr => bss_section.virt_addr, + size => bss_section.mem_size, + align => bss_section.alignment, + }; - // Vector table metadata - code.push_str("// ═══════════════════════════════════════════════════════════════\n"); - code.push_str("// Exception vector table\n"); - code.push_str("// ═══════════════════════════════════════════════════════════════\n\n"); - - if let Some(ref vectors) = sections.vector_table { - code.push_str(&format!( - r#"pub const VECTORS_META: VectorTableMeta = VectorTableMeta {{ - virt_addr: 0x{:016X}, - size: 0x{:X}, - alignment: 0x{:X}, -}}; - -"#, - vectors.virt_addr, vectors.size, vectors.alignment, - )); + if sections.vector_table.is_none() { + output::error("No vector table section for kernel, that's a bummer!"); } - // Generate loadable sections array - code.push_str("// ═══════════════════════════════════════════════════════════════\n"); - code.push_str("// Combined section array\n"); - code.push_str("// ═══════════════════════════════════════════════════════════════\n\n"); + let vector_table = sections.vector_table.as_ref().unwrap(); - code.push_str("static LOADABLE_SECTIONS: &[LoadableSection] = &[\n"); - for section in §ions.load_sections { - let meta_name = format!( - "{}_META", - section.name.trim_start_matches('.').to_uppercase() - ); - let data_name = format!( - "KERNEL_{}_BIN", - section.name.trim_start_matches('.').to_uppercase() - ); + let vector_tmpl = context! { + virt_addr => vector_table.virt_addr, + size => vector_table.mem_size, + align => vector_table.alignment, + }; - code.push_str(&format!( - " LoadableSection {{ meta: {}, data: {} }},\n", - meta_name, data_name - )); - } - code.push_str("];\n\n"); - - // Generate main kernel info - code.push_str(&format!( - r#"/// Complete kernel image information -pub static KERNEL: KernelImageInfo = KernelImageInfo {{ - virt_base: 0x{:016X}, - sections: LOADABLE_SECTIONS, - bss: {}, - vectors: {}, -}}; - -"#, - sections.virt_base, - if sections.bss_section.is_some() { - "Some(BSS_META)" - } else { - "None" - }, - if sections.vector_table.is_some() { - "Some(VECTORS_META)" - } else { - "None" - }, - )); + let context = context! { + virt_addr => sections.virt_base, + sections => sections_tmpl, + bss => bss_tmpl, + vectors => vector_tmpl, + }; // Write generated code let dest_path = out_path.join("kernel_sections.rs"); - fs::write(&dest_path, code).expect("Failed to write generated code"); + fs::write( + &dest_path, + tmpl.render(context).expect("failed to render template"), + ) + .expect("Failed to write generated code"); info!("Generated: {}", dest_path.display()); info!("Kernel virtual base: 0x{:016X}", sections.virt_base); if let Some(ref v) = sections.vector_table { info!( "Vector table: 0x{:016X} (size: 0x{:X})", - v.virt_addr, v.size + v.virt_addr, v.mem_size ); } } diff --git a/kernel/init_thread/kernel_sections.template.rs b/kernel/init_thread/kernel_sections.template.rs new file mode 100644 index 000000000..1f3ca4b50 --- /dev/null +++ b/kernel/init_thread/kernel_sections.template.rs @@ -0,0 +1,68 @@ +// Auto-generated kernel section metadata and binary includes +// DO NOT EDIT - Generated by build.rs + +#[allow(unused)] +use crate::{ + loader::{ImageInfo, SectionMeta, LoadableSection}, + memory::MemoryPermissions +}; + +// ═══════════════════════════════════════════════════════════════ +// Binary section data (included at compile time) +// ═══════════════════════════════════════════════════════════════ + +{% for section in sections %} +static {{section.bin_name}}: &[u8] = include_bytes!(concat!(env!("OUT_DIR"), "/{{section.bin_file}}")); +{% endfor %} + +// ═══════════════════════════════════════════════════════════════ +// Section metadata +// ═══════════════════════════════════════════════════════════════ + +{% for section in sections %} +pub const {{section.meta_name}}: SectionMeta = SectionMeta { + name: "{{section.name}}", + virt_addr: {{section.virt_addr | address}}, + size: {{section.size | hex}}, + alignment: {{section.align | hex}}, + permissions: MemoryPermissions { readable: {{section.r}}, writable: {{section.w}}, executable: {{section.x}} }, +}; +{% endfor %} + +pub const BSS_META: SectionMeta = SectionMeta { + name: ".bss", + virt_addr: {{bss.virt_addr | address}}, + size: {{bss.size | hex}}, + alignment: {{bss.align | hex}}, + permissions: MemoryPermissions { readable: true, writable: true, executable: false }, +}; + +// ═══════════════════════════════════════════════════════════════ +// Exception vector table +// ═══════════════════════════════════════════════════════════════ + +pub const VECTORS_META: SectionMeta = SectionMeta { + name: ".vectors", + virt_addr: {{vectors.virt_addr | address}}, + size: {{vectors.size | hex}}, + alignment: {{vectors.align | hex}}, + permissions: MemoryPermissions { readable: true, writable: false, executable: true }, +}; + +// ═══════════════════════════════════════════════════════════════ +// Combined kernel sections +// ═══════════════════════════════════════════════════════════════ + +static LOADABLE_SECTIONS: &[LoadableSection] = &[ +{% for section in sections %} + LoadableSection { meta: {{section.meta_name}}, data: {{section.bin_name}} }, +{% endfor %} +]; + +/// Complete kernel image information +pub static KERNEL: ImageInfo = ImageInfo { + virt_base: {{virt_addr | address}}, + sections: LOADABLE_SECTIONS, + bss: BSS_META, + vectors: VECTORS_META, +}; diff --git a/kernel/init_thread/src/loader.rs b/kernel/init_thread/src/loader.rs index 33d528f9f..cdff8b997 100644 --- a/kernel/init_thread/src/loader.rs +++ b/kernel/init_thread/src/loader.rs @@ -10,7 +10,7 @@ use { /// Metadata for a kernel section #[derive(Debug, Clone, Copy)] -pub struct KernelSectionMeta { +pub struct SectionMeta { /// Section name (for debugging) pub name: &'static str, /// Virtual address in kernel's higher-half address space @@ -23,7 +23,7 @@ pub struct KernelSectionMeta { pub permissions: MemoryPermissions, } -impl KernelSectionMeta { +impl SectionMeta { /// Calculate offset from kernel virtual base pub const fn offset_from_base(&self, virt_base: u64) -> u64 { self.virt_addr - virt_base @@ -40,54 +40,20 @@ impl KernelSectionMeta { } } -/// Exception vector table metadata -/// -/// The vector table must be 2KB aligned for VBAR_EL1. -/// It contains 16 entries × 128 bytes = 2048 bytes total. -#[derive(Debug, Clone, Copy)] -pub struct VectorTableMeta { - /// Virtual address of the vector table (higher-half) - pub virt_addr: u64, - /// Size of the vector table (typically 0x800 = 2048 bytes) - pub size: usize, - /// Alignment requirement (must be at least 2048 for VBAR) - pub alignment: u64, -} - -impl VectorTableMeta { - /// Calculate offset from kernel virtual base - pub const fn offset_from_base(&self, virt_base: u64) -> u64 { - self.virt_addr - virt_base - } - - /// Calculate physical address given kernel physical base - /// - /// This is the value to write to VBAR_EL1 before enabling MMU, - /// since at that point we're still using physical addresses. - pub const fn phys_addr(&self, kernel_phys_base: u64, kernel_virt_base: u64) -> u64 { - kernel_phys_base + self.offset_from_base(kernel_virt_base) // FIXME: dupe of the above same fns - } - - /// Verify the address meets VBAR alignment requirements - pub const fn is_properly_aligned(&self, addr: u64) -> bool { - addr & 0x7FF == 0 // Must be 2KB aligned - } -} - /// Complete kernel image information #[derive(Debug)] -pub struct KernelImageInfo { +pub struct ImageInfo { /// Virtual base address (higher-half) -- FIXME: don't need this necessarily pub virt_base: u64, /// Loadable sections with their binary data pub sections: &'static [LoadableSection], /// BSS section metadata (no binary data - must be zeroed) - pub bss: Option, - /// Exception vector table metadata - pub vectors: Option, + pub bss: SectionMeta, + /// Exception vector table metadata (to set up VBAR) + pub vectors: SectionMeta, } -impl KernelImageInfo { +impl ImageInfo { /// Total size needed for kernel in physical memory (all sections + BSS) pub fn total_size(&self) -> usize { let mut max_end: u64 = 0; @@ -97,10 +63,8 @@ impl KernelImageInfo { max_end = max_end.max(end); } - if let Some(bss) = &self.bss { - let bss_end = bss.virt_addr + bss.size as u64; - max_end = max_end.max(bss_end); - } + let bss_end = self.bss.virt_addr + self.bss.size as u64; + max_end = max_end.max(bss_end); let size = (max_end - self.virt_base) as usize; (size + 0xFFF) & !0xFFF // FIXME: aligned to a page size @@ -110,7 +74,7 @@ impl KernelImageInfo { /// A loadable section with its binary content #[derive(Debug)] pub struct LoadableSection { - pub meta: KernelSectionMeta, + pub meta: SectionMeta, pub data: &'static [u8], // or Option<&'static [u8]>? } @@ -118,7 +82,7 @@ pub fn load_kernel(allocator: &mut BootAllocator) -> Result Result Result Result Ok(()) } -fn zero_bss(bss: &KernelSectionMeta, kernel_phys_base: PhysAddr) -> Result<(), &'static str> { +fn zero_bss(bss: &SectionMeta, kernel_phys_base: PhysAddr) -> Result<(), &'static str> { let offset = bss.offset_from_base(KERNEL.virt_base); let dest_phys = PhysAddr::new(kernel_phys_base.as_u64() + offset); diff --git a/kernel/init_thread/src/main.rs b/kernel/init_thread/src/main.rs index 42e867e04..03093ac78 100644 --- a/kernel/init_thread/src/main.rs +++ b/kernel/init_thread/src/main.rs @@ -64,9 +64,7 @@ pub extern "C" fn init_main(_dtb_ptr: *const u8) -> ! { // Get vector table virtual address for VBAR_EL1 // VBAR is only used after MMU is enabled, so we set the virtual address directly - let vbar = kernel_layout - .vbar_el1_virt() - .expect("Kernel must define exception vector table (__vectors symbol)"); + let vbar = kernel_layout.vbar_el1_virt(); // Allocate EL1 stack let el1_stack = allocator diff --git a/kernel/init_thread/src/memory.rs b/kernel/init_thread/src/memory.rs index fe0110298..5a15ff2ef 100644 --- a/kernel/init_thread/src/memory.rs +++ b/kernel/init_thread/src/memory.rs @@ -145,9 +145,9 @@ pub struct KernelLayout { /// BSS size pub bss_size: usize, /// Exception vector table physical address (for VBAR_EL1) - pub vectors_phys: Option, + pub vectors_phys: PhysAddr, /// Exception vector table virtual address - pub vectors_virt: Option, + pub vectors_virt: VirtAddr, } impl KernelLayout { @@ -160,15 +160,13 @@ impl KernelLayout { /// /// This is what the kernel would set VBAR_EL1 to after switching to /// higher-half addresses. - pub fn vbar_el1_virt(&self) -> Option { - self.vectors_virt.map(|virt| { - assert!( - virt.as_u64() & 0x7FF == 0, - "VBAR_EL1 address 0x{:016X} must be 2KB aligned", - virt.0 - ); - virt.0 - }) + pub fn vbar_el1_virt(&self) -> u64 { + assert!( + self.vectors_virt.as_u64() & 0x7FF == 0, + "VBAR_EL1 address 0x{:016X} must be 2KB aligned", + self.vectors_virt.0 + ); + self.vectors_virt.0 } pub fn iter_sections(&self) -> impl Iterator + '_ { diff --git a/kernel/nucleus/build.rs b/kernel/nucleus/_build.rs similarity index 100% rename from kernel/nucleus/build.rs rename to kernel/nucleus/_build.rs diff --git a/libs/memory/src/arch/aarch64/virt_page.rs b/libs/memory/src/arch/aarch64/virt_page.rs index 86992edb6..fc488d173 100644 --- a/libs/memory/src/arch/aarch64/virt_page.rs +++ b/libs/memory/src/arch/aarch64/virt_page.rs @@ -292,7 +292,7 @@ mod tests { #[test_case] pub fn test_page_ranges() { let page_size = Size4KiB::SIZE; - let number = 1000usize; + let number = 1000_usize; let start_addr = VirtAddr::new(0xdeafbead); let start: Page = Page::::containing_address(start_addr); diff --git a/libs/memory/src/lib.rs b/libs/memory/src/lib.rs index 69a1056d3..6355b9848 100644 --- a/libs/memory/src/lib.rs +++ b/libs/memory/src/lib.rs @@ -13,6 +13,7 @@ #![feature(allocator_api)] #![feature(core_intrinsics)] #![feature(step_trait)] +#![feature(custom_test_frameworks)] use core::{ fmt, From 8a9220a1f50d8a9a5cde8f4e7586c15b6fb1c0a3 Mon Sep 17 00:00:00 2001 From: Berkus Decker Date: Sat, 24 Jan 2026 17:51:35 +0200 Subject: [PATCH 015/107] wip: debug qemu semihosting output - extract libprint --- Cargo.toml | 4 ++- kernel/MMU_Plan.md | 2 ++ kernel/init_thread/Cargo.toml | 1 + kernel/init_thread/src/main.rs | 4 +++ kernel/nucleus/Cargo.toml | 1 + kernel/nucleus/src/main.rs | 27 ++++++++++++++----- kernel/nucleus/src/vectors.rs | 4 --- libs/console/Cargo.toml | 1 + libs/console/src/lib.rs | 5 +--- libs/print/Cargo.toml | 27 +++++++++++++++++++ .../src/write_to.rs => print/src/lib.rs} | 15 ++++++----- libs/qemu/src/lib.rs | 25 +++++++++++++++++ 12 files changed, 93 insertions(+), 23 deletions(-) create mode 100644 libs/print/Cargo.toml rename libs/{console/src/write_to.rs => print/src/lib.rs} (83%) diff --git a/Cargo.toml b/Cargo.toml index e665fbfa9..5da79eacd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,11 +18,12 @@ members = [ "libs/memory", "libs/platform", "libs/primitives", + "libs/print", "libs/qemu", "libs/test", "libs/time", ] -resolver = "2" +resolver = "3" [workspace.package] authors = ["Berkus Decker "] @@ -58,6 +59,7 @@ libmachine = { path = "libs/machine" } libmemory = { path = "libs/memory" } libplatform = { path = "libs/platform" } libprimitives = { path = "libs/primitives" } +libprint = { path = "libs/print" } libqemu = { path = "libs/qemu" } libtest = { path = "libs/test" } libtime = { path = "libs/time" } diff --git a/kernel/MMU_Plan.md b/kernel/MMU_Plan.md index e3471b0b3..f9752ec62 100644 --- a/kernel/MMU_Plan.md +++ b/kernel/MMU_Plan.md @@ -6,6 +6,8 @@ What's needed: quick-n-dirty higher-half mappings setup and kernel physical memo Steps: - Buildable +- Print that we can invoke kernel function using a syscall from the init_thread (even it if runs at the same EL for now) + - Run steps by step - Enter kernel init in EL2 - this will be needed to set up kernel mappings - Print DTB diff --git a/kernel/init_thread/Cargo.toml b/kernel/init_thread/Cargo.toml index cd9c51351..b25f2fa7f 100644 --- a/kernel/init_thread/Cargo.toml +++ b/kernel/init_thread/Cargo.toml @@ -41,6 +41,7 @@ liblog = { workspace = true } libmachine = { workspace = true } libmemory = { workspace = true } libplatform = { workspace = true } +libprint = { workspace = true } libqemu = { workspace = true, optional = true } libtime = { workspace = true } snafu = { workspace = true } diff --git a/kernel/init_thread/src/main.rs b/kernel/init_thread/src/main.rs index 03093ac78..e37e5f1ac 100644 --- a/kernel/init_thread/src/main.rs +++ b/kernel/init_thread/src/main.rs @@ -3,6 +3,7 @@ #![no_std] #![no_main] #![allow(unused)] +#![feature(format_args_nl)] mod boot; mod el_switch; @@ -13,6 +14,7 @@ mod paging; use { core::panic::PanicInfo, + libqemu::semi_println, memory::{BootAllocator, PhysAddr}, }; @@ -24,6 +26,8 @@ unsafe extern "C" { #[unsafe(no_mangle)] pub extern "C" fn init_main(_dtb_ptr: *const u8) -> ! { + semi_println!("init_main started"); + let init_start = unsafe { &__init_start as *const u8 as u64 }; let init_end = unsafe { &__init_end as *const u8 as u64 }; let free_start = unsafe { &__free_memory_start as *const u8 as u64 }; diff --git a/kernel/nucleus/Cargo.toml b/kernel/nucleus/Cargo.toml index 5985e33ee..96303884a 100644 --- a/kernel/nucleus/Cargo.toml +++ b/kernel/nucleus/Cargo.toml @@ -44,6 +44,7 @@ liblog = { workspace = true } libmachine = { workspace = true } libmemory = { workspace = true } libplatform = { workspace = true } +libprint = { workspace = true } libqemu = { workspace = true, optional = true } libtime = { workspace = true } snafu = { workspace = true } diff --git a/kernel/nucleus/src/main.rs b/kernel/nucleus/src/main.rs index 137b664e4..0a78a0cd8 100644 --- a/kernel/nucleus/src/main.rs +++ b/kernel/nucleus/src/main.rs @@ -22,16 +22,12 @@ #![feature(ptr_internals)] #![feature(core_intrinsics)] -use core::panic::PanicInfo; -#[allow(unused_imports)] -use libconsole::{SerialOps, console::console}; use { cfg_if::cfg_if, - core::{cell::UnsafeCell, time::Duration}, + core::{arch::asm, cell::UnsafeCell, panic::PanicInfo, time::Duration}, + libcpu::endless_sleep, liblog::{info, println, warn}, - // machine::{arch, entry, memory}, - // exception, - // , time + libqemu::semi_println, }; mod vectors; @@ -40,3 +36,20 @@ mod vectors; fn panicked(info: &PanicInfo) -> ! { libmachine::panic::handler(info) } +/// Syscall entry point (the only other thing nucleus does) +#[unsafe(no_mangle)] +pub extern "C" fn syscall_handler() -> ! { + semi_println!("SYSCALL happened, we're at 0x{:016X}", get_pc()); + endless_sleep() +} + +fn get_pc() -> u64 { + let pc: u64; + unsafe { + asm!( + "adr {}, .", + out(reg) pc, + ); + } + pc +} diff --git a/kernel/nucleus/src/vectors.rs b/kernel/nucleus/src/vectors.rs index cb4e71ad5..067f4b026 100644 --- a/kernel/nucleus/src/vectors.rs +++ b/kernel/nucleus/src/vectors.rs @@ -117,9 +117,5 @@ exception_handler_serror: exception_handler_unsupported: b . - -syscall_handler: - // This is where CapInvoke syscalls arrive - b . "# ); diff --git a/libs/console/Cargo.toml b/libs/console/Cargo.toml index c416e8321..75928db3d 100644 --- a/libs/console/Cargo.toml +++ b/libs/console/Cargo.toml @@ -26,6 +26,7 @@ qemu = [] aarch64-cpu = { workspace = true } liblocking = { workspace = true } liblog = { workspace = true } +libprint = { workspace = true } libqemu = { workspace = true } libtest = { workspace = true } libtime = { workspace = true } diff --git a/libs/console/src/lib.rs b/libs/console/src/lib.rs index cbf4c67dd..d559a20b6 100644 --- a/libs/console/src/lib.rs +++ b/libs/console/src/lib.rs @@ -5,7 +5,6 @@ #![reexport_test_harness_main = "test_main"] pub mod console; -pub mod write_to; pub trait SerialOps { /// Read one byte from serial without translation. @@ -33,11 +32,9 @@ impl liblog::Log for ConsoleLogger { fn log(&self, record: &Record) { #[cfg(any(test, feature = "qemu"))] { - use crate::write_to; - let mut buf = [0_u8; 4096]; // Increase this buffer size to allow dumping larger panic texts. libqemu::semihosting::sys_write0_call( - write_to::c_show(&mut buf, *record.args()).unwrap(), + libprint::format_cstr(&mut buf, *record.args()).unwrap(), ); } diff --git a/libs/print/Cargo.toml b/libs/print/Cargo.toml new file mode 100644 index 000000000..aa5a04699 --- /dev/null +++ b/libs/print/Cargo.toml @@ -0,0 +1,27 @@ +[package] +name = "libprint" + +description = "Buffer output functions for no_std." + +authors = { workspace = true } +categories = { workspace = true } +documentation = { workspace = true } +edition = { workspace = true } +homepage = { workspace = true } +license = { workspace = true } +readme = { workspace = true } +repository = { workspace = true } +version = { workspace = true } + +publish = false + +[badges] +maintenance = { status = "experimental" } + +[dependencies] + +[lib] +test = false + +[lints] +workspace = true diff --git a/libs/console/src/write_to.rs b/libs/print/src/lib.rs similarity index 83% rename from libs/console/src/write_to.rs rename to libs/print/src/lib.rs index 285b45410..77ec5e7d7 100644 --- a/libs/console/src/write_to.rs +++ b/libs/print/src/lib.rs @@ -3,13 +3,15 @@ * Copyright (c) Berkus Decker */ +#![no_std] + /// No-alloc write!() implementation from -/// Requires you to allocate a buffer somewhere manually. +/// Requires you to allocate a buffer somewhere manually (usually, on stack). // @todo Try to use arrayvec::ArrayString here instead? // @todo probably use defmt for comms with host? use core::{cmp::min, fmt}; -pub struct WriteTo<'a> { +struct WriteTo<'a> { buffer: &'a mut [u8], // on write error (i.e. not enough space in buffer) this grows beyond // `buffer.len()`. @@ -32,7 +34,8 @@ impl<'a> WriteTo<'a> { #[allow(unused)] pub fn into_cstr(self) -> Option<&'a str> { (self.used < self.buffer.len()).then(|| { - self.buffer[self.used] = 0; // Terminate the string + // Terminate the string + self.buffer[self.used] = 0; // SAFETY: only successful concats of str - must be a valid str. unsafe { core::str::from_utf8_unchecked(&self.buffer[..=self.used]) } }) @@ -57,16 +60,14 @@ impl fmt::Write for WriteTo<'_> { } } -#[allow(unused)] -pub fn show<'a>(buffer: &'a mut [u8], args: fmt::Arguments) -> Result<&'a str, fmt::Error> { +pub fn format_str<'a>(buffer: &'a mut [u8], args: fmt::Arguments) -> Result<&'a str, fmt::Error> { let mut w = WriteTo::new(buffer); fmt::write(&mut w, args)?; w.into_str().ok_or(fmt::Error) } // Return a zero-terminated str -#[allow(unused)] -pub fn c_show<'a>(buffer: &'a mut [u8], args: fmt::Arguments) -> Result<&'a str, fmt::Error> { +pub fn format_cstr<'a>(buffer: &'a mut [u8], args: fmt::Arguments) -> Result<&'a str, fmt::Error> { let mut w = WriteTo::new(buffer); fmt::write(&mut w, args)?; w.into_cstr().ok_or(fmt::Error) diff --git a/libs/qemu/src/lib.rs b/libs/qemu/src/lib.rs index 138e1c50e..a5567685e 100644 --- a/libs/qemu/src/lib.rs +++ b/libs/qemu/src/lib.rs @@ -5,6 +5,7 @@ #![no_std] #![no_main] +#![feature(format_args_nl)] #![feature(custom_test_frameworks)] #![test_runner(libtest::test_runner)] #![reexport_test_harness_main = "test_main"] @@ -39,4 +40,28 @@ pub mod semihosting { ); } } + + #[macro_export] + macro_rules! semi_print { + // early_print!("a {} event", "log") + ($($arg:tt)+) => { + use core::{format_args}; + let mut buf = [0_u8; 4096]; // Increase this buffer size to allow dumping larger panic texts. + libqemu::semihosting::sys_write0_call( + libprint::format_cstr(&mut buf, format_args!($($arg)+)).unwrap(), + ); + } + } + + #[macro_export] + macro_rules! semi_println { + // early_println!("a {} event", "log") + ($($arg:tt)+) => { + use core::{format_args_nl}; + let mut buf = [0_u8; 4096]; // Increase this buffer size to allow dumping larger panic texts. + libqemu::semihosting::sys_write0_call( + libprint::format_cstr(&mut buf, format_args_nl!($($arg)+)).unwrap(), + ); + } + } } From f1ae781983c6d0c30f5b0294d4c649239c9e0c83 Mon Sep 17 00:00:00 2001 From: Berkus Decker Date: Sun, 25 Jan 2026 16:34:17 +0200 Subject: [PATCH 016/107] wip: run mapping and drop to EL1 --- kernel/init_thread/src/el_switch.rs | 181 ++++++++++++++----------- kernel/init_thread/src/loader.rs | 21 +++ kernel/init_thread/src/main.rs | 34 +++-- kernel/init_thread/src/memory.rs | 15 ++ kernel/init_thread/src/paging.rs | 39 +++++- kernel/init_thread/src/syscall_test.rs | 44 ++++++ kernel/nucleus/nucleus.ld | 4 +- libs/qemu/src/lib.rs | 6 +- 8 files changed, 246 insertions(+), 98 deletions(-) create mode 100644 kernel/init_thread/src/syscall_test.rs diff --git a/kernel/init_thread/src/el_switch.rs b/kernel/init_thread/src/el_switch.rs index 8d12041ca..d88934fdb 100644 --- a/kernel/init_thread/src/el_switch.rs +++ b/kernel/init_thread/src/el_switch.rs @@ -1,5 +1,16 @@ // init_thread/src/el_switch.rs - EL2 to EL1 transition with VBAR setup +use { + crate::loader::memory_barrier, + aarch64_cpu::{ + asm, + registers::{ + ELR_EL2, HCR_EL2, MAIR_EL1, ReadWriteable, SCTLR_EL1, SP_EL1, SPSR_EL2, TCR_EL1, + TTBR0_EL1, TTBR1_EL1, VBAR_EL1, Writeable, + }, + }, +}; + /// Configure and enable the MMU, set VBAR_EL1, then drop to EL1 /// /// # Arguments @@ -26,85 +37,97 @@ pub unsafe fn enable_mmu_and_drop_to_el1( // Index 1: Device-nGnRnE memory let mair: u64 = 0xFF | (0x00 << 8); - unsafe { - core::arch::asm!( - // ═══════════════════════════════════════════════════════════ - // STEP 1: Configure EL2 to allow EL1 operation - // ═══════════════════════════════════════════════════════════ - - // HCR_EL2: RW=1 means EL1 is AArch64 - "mov x0, #(1 << 31)", - "msr hcr_el2, x0", - - // ═══════════════════════════════════════════════════════════ - // STEP 2: Set up VBAR_EL1 (Exception Vector Base Address) - // ═══════════════════════════════════════════════════════════ - // - // The address must be 2KB aligned (bits [10:0] must be 0). - // We set the virtual address here since VBAR_EL1 is only - // used after MMU is enabled (exceptions before ERET would - // be taken at EL2, not EL1). - - "msr vbar_el1, {vbar}", - - // ═══════════════════════════════════════════════════════════ - // STEP 3: Configure EL1 MMU settings - // ═══════════════════════════════════════════════════════════ - - "msr mair_el1, {mair}", - - // TCR_EL1: Translation Control Register - "mov x0, #16", // T0SZ = 16 (48-bit VA for TTBR0) - "orr x0, x0, #(16 << 16)", // T1SZ = 16 (48-bit VA for TTBR1) - "orr x0, x0, #(0b10 << 30)", // TG1 = 4KB granule - "orr x0, x0, #(0b11 << 12)", // SH0 = Inner shareable - "orr x0, x0, #(0b11 << 28)", // SH1 = Inner shareable - "orr x0, x0, #(0b01 << 10)", // ORGN0 = Write-back - "orr x0, x0, #(0b01 << 26)", // ORGN1 = Write-back - "orr x0, x0, #(0b01 << 8)", // IRGN0 = Write-back - "orr x0, x0, #(0b01 << 24)", // IRGN1 = Write-back - "msr tcr_el1, x0", - - "msr ttbr0_el1, {ttbr0}", - "msr ttbr1_el1, {ttbr1}", - - // ═══════════════════════════════════════════════════════════ - // STEP 4: Prepare to drop to EL1 with MMU enabled - // ═══════════════════════════════════════════════════════════ - - // Enable MMU (takes effect after ERET) - "mrs x0, sctlr_el1", - "orr x0, x0, #1", // M = 1 - "orr x0, x0, #(1 << 2)", // C = 1 - "orr x0, x0, #(1 << 12)", // I = 1 - "msr sctlr_el1, x0", - - // SPSR_EL2: EL1h with DAIF masked - "mov x0, #0b0101", // EL1h - "orr x0, x0, #(0b1111 << 6)", // Mask DAIF - "msr spsr_el2, x0", - - // Set return address and stack - "msr elr_el2, {entry}", - "msr sp_el1, {sp}", - - "dsb sy", - "isb", - - // ═══════════════════════════════════════════════════════════ - // STEP 5: Drop to EL1 - // ═══════════════════════════════════════════════════════════ - "eret", - - mair = in(reg) mair, - ttbr0 = in(reg) ttbr0, - ttbr1 = in(reg) ttbr1, - vbar = in(reg) vbar, - entry = in(reg) entry_point, - sp = in(reg) stack_pointer, - options(noreturn, nostack) - ); - } + // ═══════════════════════════════════════════════════════════ + // STEP 1: Configure EL2 to allow EL1 operation + // ═══════════════════════════════════════════════════════════ + + // Set Hypervisor Configuration Register (EL2) + // Set EL1 execution state to AArch64 + // @todo Explain the SWIO bit (SWIO hardwired on Pi3) + HCR_EL2.write(HCR_EL2::RW::EL1IsAarch64 + HCR_EL2::SWIO::SET); + // @todo disable VM bit to prevent stage 2 MMU translations + + // ═══════════════════════════════════════════════════════════ + // STEP 2: Set up VBAR_EL1 (Exception Vector Base Address) + // ═══════════════════════════════════════════════════════════ + + // The address must be 2KB aligned (bits [10:0] must be 0). + // We set the virtual address here since VBAR_EL1 is only + // used after MMU is enabled (exceptions before ERET would + // be taken at EL2, not EL1). + VBAR_EL1.set(vbar); + + // ═══════════════════════════════════════════════════════════ + // STEP 3: Configure EL1 MMU settings + // ═══════════════════════════════════════════════════════════ + + MAIR_EL1.set(mair); + + TCR_EL1.write( + TCR_EL1::TBI0::Ignored // Top byte ignored, can be used for tagging. + // + TCR_EL1::IPS.val(ips) // Intermediate Physical Address Size + // ttbr0 user memory addresses + + TCR_EL1::TG0::KiB_4 // 4 KiB granule + + TCR_EL1::SH0::Inner + + TCR_EL1::ORGN0::WriteBack_ReadAlloc_WriteAlloc_Cacheable + + TCR_EL1::IRGN0::WriteBack_ReadAlloc_WriteAlloc_Cacheable + // + TCR_EL1::EPD0::EnableTTBR0Walks + + TCR_EL1::T0SZ.val(16) // T0SZ = 16 (48-bit VA for TTBR0) + // ttbr1 kernel memory addresses + // + TCR_EL1::TBI1::Ignored // Top byte ignored, can be used for tagging. @todo remove! + + TCR_EL1::TG1::KiB_4 // 4 KiB granule + + TCR_EL1::SH1::Inner + + TCR_EL1::ORGN1::WriteBack_ReadAlloc_WriteAlloc_Cacheable + + TCR_EL1::IRGN1::WriteBack_ReadAlloc_WriteAlloc_Cacheable + // + TCR_EL1::EPD1::DisableTTBR1Walks // @fixme disabled for now + + TCR_EL1::T1SZ.val(16), // T1SZ = 16 (48-bit VA for TTBR1) + ); + + TTBR0_EL1.set(ttbr0); + TTBR1_EL1.set(ttbr1); + + // ═══════════════════════════════════════════════════════════ + // STEP 4: Prepare to drop to EL1 with MMU enabled + // ═══════════════════════════════════════════════════════════ + + // Enable MMU (takes effect after ERET) + SCTLR_EL1.modify( + SCTLR_EL1::EE::LittleEndian // Endianness select in EL1 + + SCTLR_EL1::E0E::LittleEndian // Endianness select in EL0 + + SCTLR_EL1::WXN::Disable // Writable means Execute Never + + SCTLR_EL1::SA::Disable // SP Alignment check in EL1, 16 byte align + + SCTLR_EL1::SA0::Disable // SP Alignment check in EL0, 16 byte align + + SCTLR_EL1::A::Disable // No alignment checks + + SCTLR_EL1::UCI::Trap // Unified Cache instructions trap + + SCTLR_EL1::UCT::Trap // CTR_EL0 instructions trap + + SCTLR_EL1::UMA::Trap // User Mask Access, trap on DAIF access + + SCTLR_EL1::NTWE::Trap // WFE/WFET instruction trap + + SCTLR_EL1::NTWI::Trap // WFI/WFIT instruction trap + + SCTLR_EL1::DZE::Trap // DC ZVA/GVA/GZVA instructions trap + + SCTLR_EL1::C::Cacheable + + SCTLR_EL1::I::Cacheable + + SCTLR_EL1::M::Enable, + ); + + // SPSR_EL2: EL1h with DAIF masked + SPSR_EL2.write( + SPSR_EL2::D::Masked + + SPSR_EL2::A::Masked + + SPSR_EL2::I::Masked + + SPSR_EL2::F::Masked + + SPSR_EL2::M::EL1h, // Use SP_EL1 + ); + + // Set return address and stack + ELR_EL2.set(entry_point); + SP_EL1.set(stack_pointer); + + memory_barrier(); + + // ═══════════════════════════════════════════════════════════ + // STEP 5: Drop to EL1 + // ═══════════════════════════════════════════════════════════ + asm::eret() } /// Invalidate all TLB entries diff --git a/kernel/init_thread/src/loader.rs b/kernel/init_thread/src/loader.rs index cdff8b997..88868f3f9 100644 --- a/kernel/init_thread/src/loader.rs +++ b/kernel/init_thread/src/loader.rs @@ -6,6 +6,7 @@ use { memory::{BootAllocator, KernelLayout, MemoryPermissions, PhysAddr, VirtAddr}, }, core::ptr, + libqemu::semi_println, }; /// Metadata for a kernel section @@ -87,6 +88,11 @@ pub fn load_kernel(allocator: &mut BootAllocator) -> Result Result let offset = section.meta.offset_from_base(KERNEL.virt_base); let dest_phys = PhysAddr::new(kernel_phys_base.as_u64() + offset); + semi_println!( + "> section {}, copy {} bytes of {} bytes total to {:#016X}", + section.meta.name, + section.data.len(), + section.meta.size, + dest_phys.0 + ); + if !dest_phys.as_u64().is_multiple_of(section.meta.alignment) { return Err("Section alignment violated"); } @@ -165,6 +179,13 @@ fn zero_bss(bss: &SectionMeta, kernel_phys_base: PhysAddr) -> Result<(), &'stati let offset = bss.offset_from_base(KERNEL.virt_base); let dest_phys = PhysAddr::new(kernel_phys_base.as_u64() + offset); + semi_println!( + "> section {}, zero {} bytes at {:#016X}", + bss.name, + bss.size, + dest_phys.0 + ); + if !dest_phys.as_u64().is_multiple_of(bss.alignment) { return Err("BSS alignment violated"); } diff --git a/kernel/init_thread/src/main.rs b/kernel/init_thread/src/main.rs index e37e5f1ac..724c4bc30 100644 --- a/kernel/init_thread/src/main.rs +++ b/kernel/init_thread/src/main.rs @@ -1,5 +1,3 @@ -// init_thread/src/main.rs - #![no_std] #![no_main] #![allow(unused)] @@ -11,11 +9,14 @@ mod embed; mod loader; mod memory; mod paging; +mod syscall_test; use { core::panic::PanicInfo, + libcpu::endless_sleep, libqemu::semi_println, memory::{BootAllocator, PhysAddr}, + syscall_test::protected_call6, }; unsafe extern "C" { @@ -34,30 +35,35 @@ pub extern "C" fn init_main(_dtb_ptr: *const u8) -> ! { let memory_size = 256 * 1024 * 1024; let mut allocator = BootAllocator::new(PhysAddr::new(free_start), memory_size); + let memory_end = allocator.end(); + semi_println!( + "init_main: Created BootAllocator {memory_size} @ 0x{:016X}", + free_start + ); // ═══════════════════════════════════════════════════════════════ // PHASE 1: Load kernel // ═══════════════════════════════════════════════════════════════ - let kernel_layout = loader::load_kernel(&mut allocator).expect("Failed to load kernel"); + let kernel_layout = loader::load_kernel(&mut allocator).expect("Failed to load nucleus"); + semi_println!("init_main: Loaded nucleus image"); // ═══════════════════════════════════════════════════════════════ // PHASE 2: Set up page tables // ═══════════════════════════════════════════════════════════════ let mut mmu_setup = paging::MmuSetup::new(&mut allocator).expect("Failed to create MMU setup"); + semi_println!("init_main: Created MmuSetup"); // Identity map init_thread - paging::create_identity_mapping( - &mut mmu_setup, - PhysAddr::new(init_start), - PhysAddr::new(init_end + 0x10000), - ) - .expect("Failed to create identity mapping"); + paging::create_identity_mapping(&mut mmu_setup, PhysAddr::new(init_start), memory_end) + .expect("Failed to create identity mapping"); + semi_println!("init_main: Identity mapped the Init_Thread"); // Create kernel mapping with per-section permissions paging::create_kernel_mapping(&mut mmu_setup, &kernel_layout) .expect("Failed to create kernel mapping"); + semi_println!("init_main: Higher-half mapped the nucleus"); // ═══════════════════════════════════════════════════════════════ // PHASE 3: Prepare for EL1 @@ -65,6 +71,7 @@ pub extern "C" fn init_main(_dtb_ptr: *const u8) -> ! { let ttbr0 = mmu_setup.ttbr0(); let ttbr1 = mmu_setup.ttbr1(); + semi_println!("init_main: TTBR0_EL1 at 0x{ttbr0:016X}, TTBR1_EL1 at 0x{ttbr1:016X}"); // Get vector table virtual address for VBAR_EL1 // VBAR is only used after MMU is enabled, so we set the virtual address directly @@ -74,8 +81,9 @@ pub extern "C" fn init_main(_dtb_ptr: *const u8) -> ! { let el1_stack = allocator .alloc_pages(16) .expect("Failed to allocate EL1 stack"); - let el1_stack_top = el1_stack.as_u64() + 64 * 1024; + let el1_stack_top = el1_stack.as_u64() + 16 * 4096; // FIXME: stack must be identity-mapped! + semi_println!("init_main: EL1 stack at 0x{el1_stack_top:016X}, vbar 0x{vbar:016X}"); // ═══════════════════════════════════════════════════════════════ // PHASE 4: Enable MMU and drop to EL1 @@ -104,5 +112,9 @@ fn panic(_info: &PanicInfo) -> ! { #[unsafe(no_mangle)] pub extern "C" fn init_thread_run(_dtb_ptr: *const u8) -> ! { // Run initial thread further in EL1, seting up the capDL etc. - panic!("continue system init here"); + semi_println!("init_main_run dropped to EL1"); + unsafe { + protected_call6(0, 0, 0, 0, 0, 0, 0, 0); + } + endless_sleep() } diff --git a/kernel/init_thread/src/memory.rs b/kernel/init_thread/src/memory.rs index 5a15ff2ef..3f726aaab 100644 --- a/kernel/init_thread/src/memory.rs +++ b/kernel/init_thread/src/memory.rs @@ -88,6 +88,9 @@ impl BootAllocator { pub fn current(&self) -> PhysAddr { self.current } + pub fn end(&self) -> PhysAddr { + self.end + } pub fn remaining(&self) -> usize { (self.end.0 - self.current.0) as usize } @@ -101,6 +104,18 @@ pub struct MemoryPermissions { pub executable: bool, } +impl core::fmt::Display for MemoryPermissions { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + write!( + f, + "{}{}{}", + if self.readable { "R" } else { "-" }, + if self.writable { "W" } else { "-" }, + if self.executable { "X" } else { "-" } + ) + } +} + impl MemoryPermissions { /// Convert to AArch64 page table flags pub const fn as_pte_flags(&self) -> u64 { diff --git a/kernel/init_thread/src/paging.rs b/kernel/init_thread/src/paging.rs index a222b1e57..604358a1e 100644 --- a/kernel/init_thread/src/paging.rs +++ b/kernel/init_thread/src/paging.rs @@ -5,6 +5,7 @@ use { BootAllocator, KernelLayout, MemoryPermissions, PhysAddr, SectionMapping, VirtAddr, }, core::ptr, + libqemu::semi_println, }; /// Page table entry flags for AArch64 Stage 1 @@ -92,7 +93,7 @@ impl<'a> MmuSetup<'a> { perms: MemoryPermissions, ) -> Result<(), &'static str> { let pte_flags = perms.as_pte_flags() | flags::ATTR_NORMAL; - self.map_page_with_flags(ttbr, virt, phys, pte_flags) + self.map_page_with_flags(ttbr, virt, phys, pte_flags, perms) } /// Map a 4KB page with raw PTE flags @@ -102,6 +103,7 @@ impl<'a> MmuSetup<'a> { virt: VirtAddr, phys: PhysAddr, pte_flags: u64, + perms: MemoryPermissions, // Only for Display ) -> Result<(), &'static str> { let l0_phys = match ttbr { Ttbr::Ttbr0 => self.ttbr0_l0, @@ -121,6 +123,17 @@ impl<'a> MmuSetup<'a> { let l3_table = unsafe { &mut *(l3_phys.as_mut_ptr::()) }; l3_table.entries[l3_idx] = phys.as_u64() | flags::VALID | flags::PAGE | pte_flags; + semi_println!( + "Mapped 4K page {:#016X} frame {:#016X} in {} with {}", + virt.0, + phys.0, + match ttbr { + Ttbr::Ttbr0 => "TTBR0(user)", + Ttbr::Ttbr1 => "TTBR1(kernel)", + }, + perms + ); + Ok(()) } @@ -154,6 +167,17 @@ impl<'a> MmuSetup<'a> { let l2_table = unsafe { &mut *(l2_phys.as_mut_ptr::()) }; l2_table.entries[l2_idx] = phys.as_u64() | flags::VALID | flags::BLOCK | pte_flags; + semi_println!( + "Mapped 2M page {:#016X} frame {:#016X} in {} with {}", + virt.0, + phys.0, + match ttbr { + Ttbr::Ttbr0 => "TTBR0(user)", + Ttbr::Ttbr1 => "TTBR1(kernel)", + }, + perms + ); + Ok(()) } @@ -235,6 +259,11 @@ pub fn create_kernel_mapping( /// Map a single section with proper permissions fn map_section(setup: &mut MmuSetup, section: &SectionMapping) -> Result<(), &'static str> { + if !section.phys_start.is_aligned(4096) { + semi_println!("!! Section {} not aligned to 4K boundary!", section.name); + return Err("Section not aligned"); + } + // Check if we can use 2MB blocks (section must be 2MB aligned and sized) let can_use_2mb = section.phys_start.is_aligned(2 * 1024 * 1024) && section.virt_start.as_u64() % (2 * 1024 * 1024) == 0 @@ -293,7 +322,13 @@ pub fn create_device_mapping( // Use device memory attributes let pte_flags = perms.as_pte_flags() | flags::ATTR_DEVICE; - setup.map_page_with_flags(Ttbr::Ttbr1, VirtAddr::new(va), PhysAddr::new(pa), pte_flags)?; + setup.map_page_with_flags( + Ttbr::Ttbr1, + VirtAddr::new(va), + PhysAddr::new(pa), + pte_flags, + perms, + )?; } Ok(()) diff --git a/kernel/init_thread/src/syscall_test.rs b/kernel/init_thread/src/syscall_test.rs new file mode 100644 index 000000000..5193bdc2f --- /dev/null +++ b/kernel/init_thread/src/syscall_test.rs @@ -0,0 +1,44 @@ +/// Single syscall ABI +/// +/// Entry: SVC #0 +/// +/// Arguments: +/// x0 = capability slot +/// x1 = operation code +/// x2-x7 = operation arguments (6 args!) +/// x9-x15 are caller-saved, we don't use them +/// +/// Returns: +/// x0 = error code (0 = success) +/// x1 = return value 0 +/// x2 = return value 1 (if needed) +#[inline(always)] +pub unsafe fn protected_call6( + cap: u32, + op: u32, + a0: u64, + a1: u64, + a2: u64, + a3: u64, + a4: u64, + a5: u64, +) -> (u64, u64, u64) { + let r0: u64; + let r1: u64; + let r2: u64; + unsafe { + core::arch::asm!( + "svc #0", + inlateout("x0") cap as u64 => r0, + inlateout("x1") op as u64 => r1, + inlateout("x2") a0 => r2, + in("x3") a1, + in("x4") a2, + in("x5") a3, + in("x6") a4, + in("x7") a5, + options(nostack), + ); + } + (r0, r1, r2) +} diff --git a/kernel/nucleus/nucleus.ld b/kernel/nucleus/nucleus.ld index 9ec87069c..91442e763 100644 --- a/kernel/nucleus/nucleus.ld +++ b/kernel/nucleus/nucleus.ld @@ -41,8 +41,8 @@ SECTIONS . = ALIGN(2K); } :segment_code - /* TODO: rodata is currently executable, but doesn't have to... */ - .rodata : /*AT(ADDR(.rodata) - __KERNEL_VIRT_ADDR_BASE)*/ ALIGN(4) + /* TODO: rodata is currently executable, but doesn't have to... also each individual section must be aligned to 4K at least for mappings to work */ + .rodata : /*AT(ADDR(.rodata) - __KERNEL_VIRT_ADDR_BASE)*/ ALIGN(__PAGE_SIZE) { *(.rodata*) FILL(0x00) diff --git a/libs/qemu/src/lib.rs b/libs/qemu/src/lib.rs index a5567685e..9873cb3d3 100644 --- a/libs/qemu/src/lib.rs +++ b/libs/qemu/src/lib.rs @@ -45,10 +45,9 @@ pub mod semihosting { macro_rules! semi_print { // early_print!("a {} event", "log") ($($arg:tt)+) => { - use core::{format_args}; let mut buf = [0_u8; 4096]; // Increase this buffer size to allow dumping larger panic texts. libqemu::semihosting::sys_write0_call( - libprint::format_cstr(&mut buf, format_args!($($arg)+)).unwrap(), + libprint::format_cstr(&mut buf, core::format_args!($($arg)+)).unwrap(), ); } } @@ -57,10 +56,9 @@ pub mod semihosting { macro_rules! semi_println { // early_println!("a {} event", "log") ($($arg:tt)+) => { - use core::{format_args_nl}; let mut buf = [0_u8; 4096]; // Increase this buffer size to allow dumping larger panic texts. libqemu::semihosting::sys_write0_call( - libprint::format_cstr(&mut buf, format_args_nl!($($arg)+)).unwrap(), + libprint::format_cstr(&mut buf, core::format_args_nl!($($arg)+)).unwrap(), ); } } From b125803be05fa4e41334f8690a3702b61045697e Mon Sep 17 00:00:00 2001 From: Berkus Decker Date: Sun, 25 Jan 2026 18:13:13 +0200 Subject: [PATCH 017/107] wip: working syscall EL1-to-EL1 (temporary?) --- kernel/nucleus/src/vectors.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kernel/nucleus/src/vectors.rs b/kernel/nucleus/src/vectors.rs index 067f4b026..4690d50ed 100644 --- a/kernel/nucleus/src/vectors.rs +++ b/kernel/nucleus/src/vectors.rs @@ -44,7 +44,7 @@ curr_el_sp0_serror: .balign 128 curr_el_spx_sync: - b exception_handler_sync + b syscall_handler .balign 128 curr_el_spx_irq: From 93bba3a029f2a707b80cf78e13f7efc708660ad8 Mon Sep 17 00:00:00 2001 From: Berkus Decker Date: Sun, 25 Jan 2026 18:35:11 +0200 Subject: [PATCH 018/107] wip: panic handler --- kernel/init_thread/src/main.rs | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/kernel/init_thread/src/main.rs b/kernel/init_thread/src/main.rs index 724c4bc30..dd12e8add 100644 --- a/kernel/init_thread/src/main.rs +++ b/kernel/init_thread/src/main.rs @@ -37,7 +37,7 @@ pub extern "C" fn init_main(_dtb_ptr: *const u8) -> ! { let mut allocator = BootAllocator::new(PhysAddr::new(free_start), memory_size); let memory_end = allocator.end(); semi_println!( - "init_main: Created BootAllocator {memory_size} @ 0x{:016X}", + "init_main: Created BootAllocator {memory_size} @ {:#016X}", free_start ); @@ -71,7 +71,7 @@ pub extern "C" fn init_main(_dtb_ptr: *const u8) -> ! { let ttbr0 = mmu_setup.ttbr0(); let ttbr1 = mmu_setup.ttbr1(); - semi_println!("init_main: TTBR0_EL1 at 0x{ttbr0:016X}, TTBR1_EL1 at 0x{ttbr1:016X}"); + semi_println!("init_main: TTBR0_EL1 at {ttbr0:#016X}, TTBR1_EL1 at {ttbr1:#016X}"); // Get vector table virtual address for VBAR_EL1 // VBAR is only used after MMU is enabled, so we set the virtual address directly @@ -83,7 +83,7 @@ pub extern "C" fn init_main(_dtb_ptr: *const u8) -> ! { .expect("Failed to allocate EL1 stack"); let el1_stack_top = el1_stack.as_u64() + 16 * 4096; // FIXME: stack must be identity-mapped! - semi_println!("init_main: EL1 stack at 0x{el1_stack_top:016X}, vbar 0x{vbar:016X}"); + semi_println!("init_main: EL1 stack at {el1_stack_top:#016X}, vbar {vbar:#016X}"); // ═══════════════════════════════════════════════════════════════ // PHASE 4: Enable MMU and drop to EL1 @@ -101,12 +101,9 @@ pub extern "C" fn init_main(_dtb_ptr: *const u8) -> ! { } #[panic_handler] -fn panic(_info: &PanicInfo) -> ! { - loop { - unsafe { - core::arch::asm!("wfe"); - } - } +fn panic(info: &PanicInfo) -> ! { + semi_println!("PANICKED: {info}"); + endless_sleep() } #[unsafe(no_mangle)] From aaf6554c5d3345972b45a44268dcbcf833eb566a Mon Sep 17 00:00:00 2001 From: Berkus Decker Date: Tue, 13 Jan 2026 14:44:20 +0200 Subject: [PATCH 019/107] wip: add kernel api --- kernel/nucleus/src/api/buffer.rs | 327 ++++++++++++ kernel/nucleus/src/api/debug_console.rs | 44 ++ kernel/nucleus/src/api/domain.rs | 359 +++++++++++++ kernel/nucleus/src/api/endpoint.rs | 495 ++++++++++++++++++ kernel/nucleus/src/api/event_count.rs | 103 ++++ kernel/nucleus/src/api/key.rs | 26 + kernel/nucleus/src/api/key_table.rs | 140 +++++ kernel/nucleus/src/api/mod.rs | 20 + kernel/nucleus/src/api/notification.rs | 81 +++ kernel/nucleus/src/api/reply.rs | 194 +++++++ kernel/nucleus/src/api/syscall.rs | 128 +++++ kernel/nucleus/src/api/time.rs | 105 ++++ kernel/nucleus/src/api/untyped.rs | 99 ++++ kernel/nucleus/src/main.rs | 150 +++++- kernel/nucleus/src/vectors.rs | 2 +- kernel/tests/memory.rs | 20 +- .../raspberrypi/linker/nucleus-new.ld | 53 ++ 17 files changed, 2321 insertions(+), 25 deletions(-) create mode 100644 kernel/nucleus/src/api/buffer.rs create mode 100644 kernel/nucleus/src/api/debug_console.rs create mode 100644 kernel/nucleus/src/api/domain.rs create mode 100644 kernel/nucleus/src/api/endpoint.rs create mode 100644 kernel/nucleus/src/api/event_count.rs create mode 100644 kernel/nucleus/src/api/key.rs create mode 100644 kernel/nucleus/src/api/key_table.rs create mode 100644 kernel/nucleus/src/api/mod.rs create mode 100644 kernel/nucleus/src/api/notification.rs create mode 100644 kernel/nucleus/src/api/reply.rs create mode 100644 kernel/nucleus/src/api/syscall.rs create mode 100644 kernel/nucleus/src/api/time.rs create mode 100644 kernel/nucleus/src/api/untyped.rs create mode 100644 libs/platform/src/platform/raspberrypi/linker/nucleus-new.ld diff --git a/kernel/nucleus/src/api/buffer.rs b/kernel/nucleus/src/api/buffer.rs new file mode 100644 index 000000000..81f041ed3 --- /dev/null +++ b/kernel/nucleus/src/api/buffer.rs @@ -0,0 +1,327 @@ +// ================================================== +// == Public user interface, usable from userspace == +// ================================================== + +/// Buffer capability with permission tracking in type system +pub struct BufferKey { + key: Key, + size: usize, + _perm: PhantomData

, +} + +pub trait Permission {} +pub struct ReadOnly; +pub struct ReadWrite; +impl Permission for ReadOnly {} +impl Permission for ReadWrite {} + +impl BufferKey

{ + /// Map buffer into current domain's address space. + /// + /// # Arguments + /// * `hint` - Optional preferred virtual address (kernel may ignore) + /// * `flags` - Mapping flags (caching behavior, etc.) + /// + /// # Returns + /// Virtual address where buffer is mapped + pub fn map(&self, hint: Option, flags: MapFlags) -> Result { + let hint_val = hint.map(|a| a.as_u64()).unwrap_or(0); + + let ret = unsafe { + protected_call2( + self.key.slot as u64, + BufferOp::Map as u64, + hint_val, + // self.size, ? + flags.bits() as u64, + ) + }; + + if ret == 0 { + Err(Error::MapFailed) + } else { + Ok(VirtAddr::new(ret)) + } + } + + /// Unmap buffer from current domain's address space. + pub fn unmap(&self) -> Result<(), Error> { + let ret = unsafe { protected_call1(self.key.slot as u64, BufferOp::Unmap as u64, 0) }; + Error::from_code(ret) + } + + /// Query buffer information (no mapping required) + pub fn query(&self) -> Result { + let mut info = BufferInfo::default(); + let ret = unsafe { + protected_call2( + self.key.slot as u64, + BufferOp::Query as u64, + &mut info as *mut _ as u64, + 0, + ) + }; + if ret == 0 { + Ok(info) + } else { + Err(Error::from_code(ret)) + } + } +} + +// Other ops +impl BufferKey

{ + /// Size of this buffer + #[inline] + pub fn size(&self) -> usize { + self.size + } +} + +impl BufferKey { + /// Derive read-only capability (like &T from &mut T) + pub fn derive_readonly(&self, dest_slot: u32) -> Result, Error> { + let ret = unsafe { + protected_call3( + CAPTBL_SELF, + KeyTableOp::CopyDerive, + self.key.slot as u64, + dest_slot as u64, + Rights::READ.bits() as u64, + ) + }; + if ret == 0 { + Ok(BufferKey { + key: Cap::new(dest_slot), + size: self.size, + _perm: PhantomData, + }) + } else { + Err(Error::from_code(ret)) + } + } + + /// Map and get mutable slice + pub fn map_slice_mut(&self) -> Result, Error> { + let addr = self.map(None, MapFlags::NONE)?; + Ok(MappedSliceMut { + cap: self, + ptr: addr.as_mut_ptr(), + len: self.size, + }) + } +} + +impl BufferKey { + /// Map and get immutable slice + pub fn map_slice(&self) -> Result, Error> { + let addr = self.map(None, MapFlags::NONE)?; + Ok(MappedSlice { + cap: self, + ptr: addr.as_ptr(), + len: self.size, + }) + } +} + +#[derive(Default)] +pub struct BufferInfo { + pub size: usize, + pub flags: BufferFlags, + pub is_mapped: bool, + pub mapped_addr: Option, +} + +bitflags::bitflags! { + pub struct MapFlags: u32 { + const NONE = 0; + const FIXED = 1 << 0; // Fail if hint can't be used + const POPULATE = 1 << 1; // Pre-fault all pages + const UNCACHED = 1 << 2; // Override to uncached + } +} + +/// RAII guard that unmaps on drop +pub struct MappedSlice<'a> { + cap: &'a BufferKey, + ptr: *const u8, + len: usize, +} + +impl<'a> MappedSlice<'a> { + pub fn as_slice(&self) -> &[u8] { + unsafe { core::slice::from_raw_parts(self.ptr, self.len) } + } +} + +impl Drop for MappedSlice<'_> { + fn drop(&mut self) { + let _ = self.cap.unmap(); + } +} + +pub struct MappedSliceMut<'a> { + cap: &'a BufferKey, + ptr: *mut u8, + len: usize, +} + +impl<'a> MappedSliceMut<'a> { + pub fn as_slice(&self) -> &[u8] { + unsafe { core::slice::from_raw_parts(self.ptr, self.len) } + } + + pub fn as_mut_slice(&mut self) -> &mut [u8] { + unsafe { core::slice::from_raw_parts_mut(self.ptr, self.len) } + } +} + +impl Drop for MappedSliceMut<'_> { + fn drop(&mut self) { + let _ = self.cap.unmap(); + } +} + +// ============================================== +// == Kernel space object and syscall handling == +// ============================================== + +/// Buffer kernel object - represents a contiguous memory region +pub struct Buffer { + phys_base: PhysAddr, // Physical address (fixed at creation) + size: usize, // Size in bytes (fixed at creation) + flags: BufferFlags, // CACHED, DEVICE, SHARED, etc. + + // Mapping tracking (for single-address-space) + // mappings: SmallVec, // Who has it mapped where +} + +struct Mapping { + domain_id: DomainId, + virt_addr: VirtAddr, + permissions: Rights, // May be less than cap rights (derived cap) +} + +bitflags::bitflags! { + pub struct BufferFlags: u32 { + const CACHED = 1 << 0; // Normal cacheable memory + const DEVICE = 1 << 1; // Device memory (uncached, no speculative) + const SHARED = 1 << 2; // Multi-domain sharing expected + const DMA = 1 << 3; // DMA-capable (physically contiguous) + const EXEC = 1 << 4; // Executable (if supported) + } +} + +/// Buffer operations via protected_call +#[repr(u8)] +enum BufferOp { + Map = 0, // Map into caller's address space + Unmap = 1, // Remove mapping + Query = 2, // Get buffer info (size, flags, mapping status) +} + +// ===================== +// == Syscall handler == +// ===================== + +pub fn invoke(cap: &CapEntry, op: u32, arg0: u64, arg1: u64) -> SyscallResult { + let buffer = cap.as_buffer()?; + let caller = current_domain(); + + match BufferOp::try_from(op)? { + BufferOp::Map => { + // Check MAP right + if !cap.rights.contains(Rights::MAP) { + return Err(SyscallError::PermissionDenied); + } + + // Check not already mapped by this domain + if buffer.is_mapped_by(caller.id) { + return Err(SyscallError::AlreadyMapped); + } + + let hint = if arg0 != 0 { + Some(VirtAddr::new(arg0)) + } else { + None + }; + let flags = MapFlags::from_bits_truncate(arg1 as u32); + + // Allocate virtual address range + let vaddr = caller.address_space.allocate_range( + hint, + buffer.size, + flags.contains(MapFlags::FIXED), + )?; + + // Compute page table permissions from cap rights + let pte_flags = cap.rights.to_pte_flags() | buffer.flags.to_pte_flags(); + + // Install page table mappings + for offset in (0..buffer.size).step_by(PAGE_SIZE) { + let paddr = buffer.phys_base + offset; + let vaddr_page = vaddr + offset; + + caller + .address_space + .map_page(vaddr_page, paddr, pte_flags)?; + } + + // Record mapping for revocation + buffer.mappings.push(Mapping { + domain_id: caller.id, + virt_addr: vaddr, + permissions: cap.rights, + }); + + Ok(vaddr.as_u64()) + } + + BufferOp::Unmap => { + // Find and remove mapping for this domain + let mapping = buffer + .mappings + .iter() + .position(|m| m.domain_id == caller.id) + .ok_or(SyscallError::NotMapped)?; + + let mapping = buffer.mappings.remove(mapping); + + // Remove page table entries + for offset in (0..buffer.size).step_by(PAGE_SIZE) { + caller + .address_space + .unmap_page(mapping.virt_addr + offset)?; + } + + // TLB invalidation (in single-address-space, this is local) + tlb_invalidate_range(mapping.virt_addr, buffer.size); + + Ok(0) + } + + BufferOp::Query => { + let info_ptr = arg0 as *mut BufferInfo; + + // Validate user pointer + if !caller.address_space.is_valid_user_ptr(info_ptr) { + return Err(SyscallError::InvalidPointer); + } + + let mapping = buffer.mappings.iter().find(|m| m.domain_id == caller.id); + + let info = BufferInfo { + size: buffer.size, + flags: buffer.flags, + is_mapped: mapping.is_some(), + mapped_addr: mapping.map(|m| m.virt_addr), + }; + + unsafe { + info_ptr.write(info); + } + + Ok(0) + } + } +} diff --git a/kernel/nucleus/src/api/debug_console.rs b/kernel/nucleus/src/api/debug_console.rs new file mode 100644 index 000000000..a560ec8fa --- /dev/null +++ b/kernel/nucleus/src/api/debug_console.rs @@ -0,0 +1,44 @@ +// ================================================== +// == Public user interface, usable from userspace == +// ================================================== + +pub struct DebugConsoleKey { + key: Key, +} + +// Root domain gets a DebugConsoleCap, can delegate to others +impl DebugConsoleKey { + pub fn write(&self, s: &str) -> Result<()> { + protected_call2( + self.slot, + DebugConsoleOp::Write as u32, + s.as_ptr() as u64, + s.len() as u64, + ) + } +} + +// ============================================== +// == Kernel space object and syscall handling == +// ============================================== + +struct DebugConsole; + +impl DebugConsole { + fn handle_write() {} +} + +#[repr(u8)] +pub enum DebugConsoleOp { + Write = 0, +} + +// ===================== +// == Syscall handler == +// ===================== + +pub fn invoke(cap: &CapEntry, op: u32, arg0: u64, arg1: u64) -> SyscallResult { + match op { + _ => Err(SyscallError::InvalidOp), + } +} diff --git a/kernel/nucleus/src/api/domain.rs b/kernel/nucleus/src/api/domain.rs new file mode 100644 index 000000000..cdac1dc26 --- /dev/null +++ b/kernel/nucleus/src/api/domain.rs @@ -0,0 +1,359 @@ +use std::sync::atomic::Ordering; + +// ================================================== +// == Public user interface, usable from userspace == +// ================================================== + +// CSpace layout with self-reference +pub const CAPTBL_SELF: u32 = 0; // Every domain has cap to own captbl here + +/// Domain capability - handle to a protection domain. +/// State queries use shared DCB (no syscall), mutations use CapInvoke. +pub struct DomainCap { + cap: Cap, + id: DomainId, +} + +impl DomainCap { + /// Create a new domain from untyped memory. + /// Convenience wrapper around UntypedRetype. + pub fn create(untyped: &mut UntypedCap, dest_slot: CapSlot) -> Result { + // Domains need ~4KB (12 bits) for kernel structures + untyped_retype( + untyped.split(12)?, // Carve off 4KB + ObjectType::Domain, + 12, + dest_slot, + )?; + + // Domain ID is returned in secondary return value + // (or we query it from the newly created DCB) + Ok(DomainCap { + cap: Cap::new(dest_slot), + id: DomainId(/* ... */), + }) + } + + /// Get domain state from shared DCB + #[inline] + pub fn state(&self) -> DomainState { + let dcb = DCB.get(self.id); + DomainState::try_from(dcb.state.load(Ordering::Acquire)).unwrap_or(DomainState::Inactive) + } + + /// Get time used from shared DCB + #[inline] + pub fn time_used_ns(&self) -> u64 { + let dcb = DCB.get(self.id); + dcb.time_used_ns.load(Ordering::Relaxed) + } + + /// Get pending notifications from shared DCB (NO SYSCALL!) + #[inline] + pub fn pending_notifications(&self) -> u64 { + let dcb = DCB.get(self.id); + dcb.pending_notifications.load(Ordering::Relaxed) + } + + /// Activate domain (make runnable) - requires syscall + pub fn activate(&self) -> Result<(), Error> { + let ret = unsafe { syscall3(self.cap.slot as u64, DomainOp::Activate as u64, 0) }; + Error::from_code(ret) + } + + /// Grant a capability to this domain - requires syscall + pub fn grant(&self, cap: &Cap, dest_slot: CapSlot) -> Result<(), Error> { + let ret = unsafe { + syscall4( + self.cap.slot as u64, + DomainOp::Grant as u64, + cap.slot() as u64, + dest_slot as u64, + ) + }; + Error::from_code(ret) + } + + /// Suspend domain - requires syscall + pub fn suspend(&self) -> Result<(), Error> { + let ret = unsafe { syscall3(self.cap.slot as u64, DomainOp::Suspend as u64, 0) }; + Error::from_code(ret) + } + + /// Resume suspended domain - requires syscall + pub fn resume(&self) -> Result<(), Error> { + let ret = unsafe { syscall3(self.cap.slot as u64, DomainOp::Resume as u64, 0) }; + Error::from_code(ret) + } +} + +// ============================================== +// == Kernel space object and syscall handling == +// ============================================== + +struct Domain; + +impl Domain { + // Initialize new domain's cspace + fn init_cspace(&mut self) { + // Slot 0: capability to this captbl itself + self.cspace[CAPTBL_SELF] = Cap::new(ObjectType::KeyTable, self.cspace_id); + // Now domain can manipulate its own caps + } +} + +#[repr(u8)] +pub enum DomainOp { + Activate = 0, // Make domain runnable + Grant = 1, // Grant capability to domain + Suspend = 2, // Suspend domain + Resume = 3, // Resume suspended domain +} + +#[repr(u32)] +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub enum DomainState { + /// Just created, never run + Inactive = 0, + /// Ready to receive CPU time + Runnable = 1, + /// Currently executing (only one domain per CPU) + Running = 2, + /// Waiting on notification/event/endpoint + Blocked = 3, + /// Explicitly suspended by parent + Suspended = 4, + /// Faulted, needs handler + Faulted = 5, +} + +#[repr(u32)] +#[derive(Copy, Clone, Debug)] +pub enum BlockReason { + None = 0, + Notification = 1, // NotifyCap::wait() + EventCount = 2, // EventCountCap::await_ge() + Endpoint = 3, // EndpointCap::call() or recv() + TimeDonated = 4, // Donated time, waiting for return +} + +// pub enum DeactivateReason { +// TimeExhausted, +// BlockedOnEvent(EndpointCap), <-- BlockReason::Endpoint +// Yielded, +// Faulted(fault), +// } + +/// Domain Control Block +/// Our DCB structure - combining Nemesis ideas with our capability model +/// +/// Key insight: split into kernel-private and shared sections +#[repr(C, align(128))] // Cache-line aligned +pub struct DomainControlBlock { + // ═══════════════════════════════════════════════════════════ + // SHARED SECTION (read-only mapped to userspace) + // ═══════════════════════════════════════════════════════════ + // ─── Identity ─── + pub id: DomainId, + pub name: [u8; 24], + + // ─── Execution State (Acquire/Release on state field) ─── + pub state: AtomicU32, // DomainState + pub block_reason: AtomicU32, // BlockReason (if blocked) + pub blocked_on: AtomicU32, // Cap slot we're blocked on + + // ─── Time Accounting (QoS) ─── + /// Cumulative CPU time consumed (nanoseconds) + pub time_used_ns: AtomicU64, + /// Time remaining in current activation + pub time_remaining_ns: AtomicU64, + /// Number of times activated + pub activation_count: AtomicU64, + + // ─── Event State ─── + pub pending_notifications: AtomicU64, // Bitmap of pending notify caps + /// Number of pending events (sum across all endpoints) + pub pending_events: AtomicU32, // Number of event counts with data + + /// Endpoint that caused last wakeup + pub last_event_ep: AtomicU32, + + // ─── Scheduling Parameters ─── + /// Parent scheduler domain + pub scheduler: DomainId, + /// Scheduling priority/parameters + pub priority: u32, + /// Allocation period (for periodic domains) + pub period_ns: u64, + /// CPU allocation per period + pub budget_ns: u64, // slice_ns + + /// Scheduled deadline (absolute time) + pub deadline: AtomicU64, + + // ─── Fault Information ─── + /// Last fault type (if any) + pub fault_type: AtomicU32, + + /// Fault address + pub fault_addr: AtomicU64, + + pub fault_cap: AtomicU32, // Cap slot that caused fault + + // Padding to cache line + _pad: [u8; 16], + // ═══════════════════════════════════════════════════════════ + // PRIVATE SECTION (kernel only, NOT mapped to userspace) + // ═══════════════════════════════════════════════════════════ + // + // This would be in a separate structure or after a page boundary + // - Saved register context + // - Capability space root + // - Kernel stack pointer + // - Etc. +} + +// Verify size for cache alignment +const _: () = assert!(core::mem::size_of::() == 128); + +// 32 DCBs per 4KB page +// Multiple pages for more domains + +// Userspace sees: const DCB_BASE: *const DomainControlBlock = 0xFFFF_0000_0000_0000; // FIXME: pervasives +// Access DCB n: &*DCB_BASE.add(n) + +/// Userspace view of DCB array +/// Mapped read-only at a well-known address +pub struct DcbView { + base: *const DomainControlBlock, +} + +impl DcbView { + /// Get from well-known address (set up by kernel at domain creation) + pub const fn new() -> Self { + Self { + base: 0xFFFF_0000_0000_0000 as *const DomainControlBlock, + } + } + + /// Read any domain's state + #[inline(always)] + pub fn get(&self, id: DomainId) -> &DomainControlBlock { + unsafe { &*self.base.add(id.0 as usize) } + } + + /// Get my own DCB + #[inline(always)] + pub fn myself(&self) -> &DomainControlBlock { + // Current domain ID stored in thread-local or well-known register + self.get(current_domain_id()) + } +} + +// Domain scheduling support in kernel: +impl Nucleus { + /// Called when domain is activated (receives CPU time) + fn activate_domain(&mut self, id: DomainId, time_budget: u64) { + let dcb = self.dcb_mut(id); + + dcb.state + .store(DomainState::Running as u32, Ordering::Release); + dcb.time_remaining_ns.store(time_budget, Ordering::Release); + dcb.deadline.store(now() + time_budget, Ordering::Release); + } + + /// Called on every context switch FROM this domain + fn deactivate_domain(&mut self, id: DomainId, reason: DeactivateReason) { + let dcb = self.dcb_mut(id); + let elapsed = /* calculate from timer */0; + + // Update time accounting + dcb.time_used_ns.fetch_add(elapsed, Ordering::Relaxed); + dcb.time_remaining_ns.fetch_sub(elapsed, Ordering::Relaxed); + + // Update state + match reason { + DeactivateReason::TimeExhausted => { + dcb.state + .store(DomainState::Runnable as u32, Ordering::Release); + } + DeactivateReason::BlockedOnEvent(ep) => { + dcb.state + .store(DomainState::Blocked as u32, Ordering::Release); + dcb.block_reason + .store(BlockReason::Event as u32, Ordering::Release); + dcb.last_event_ep.store(ep, Ordering::Release); + } + DeactivateReason::Yielded => { + dcb.state + .store(DomainState::Runnable as u32, Ordering::Release); + } + DeactivateReason::Faulted(fault) => { + dcb.state + .store(DomainState::Faulted as u32, Ordering::Release); + dcb.fault_type.store(fault.type_code(), Ordering::Release); + dcb.fault_addr.store(fault.addr(), Ordering::Release); + } + } + } + + /// Called when event arrives for blocked domain + fn signal_domain(&mut self, id: DomainId) { + let dcb = self.dcb_mut(id); + + dcb.pending_events.fetch_add(1, Ordering::Release); + + // If blocked on events, make runnable + if dcb.state.load(Ordering::Acquire) == DomainState::Blocked as u32 { + dcb.state + .store(DomainState::Runnable as u32, Ordering::Release); + } + } +} + +// ## Memory Ordering Considerations + +// KERNEL (writer) USERSPACE (reader) +// ─────────────── ────────────────── + +// // Update multiple fields +// dcb.time_used.store(x, Relaxed); +// dcb.time_remaining.store(y, Relaxed); +// dcb.state.store(z, Release); ──────────────────────┐ +// │ │ +// │ Release ensures all │ +// │ prior writes visible │ +// ▼ ▼ +// let state = dcb.state.load(Acquire); +// // Acquire ensures we see +// // all writes before the Release +// let used = dcb.time_used.load(Relaxed); +// let rem = dcb.time_remaining.load(Relaxed); + +// Protocol: +// - Kernel does Release store on state LAST +// - Userspace does Acquire load on state FIRST +// - Then can safely read other fields with Relaxed + +// ===================== +// == Syscall handler == +// ===================== + +pub fn invoke(cap: &Cap, op: u32, arg0: u64, arg1: u64) -> SyscallResult { + let domain = cap.as_domain()?; + match op { + DomainOp::Activate => { + // Make domain runnable (usually combined with TimeCap donation) + domain.activate() + } + DomainOp::Grant => { + // Grant a capability to this domain's cspace + let src_slot = arg0 as CapSlot; + let dest_slot = arg1 as CapSlot; + domain.grant_cap(src_slot, dest_slot) + } + DomainOp::Suspend => domain.suspend(), + DomainOp::Resume => domain.resume(), + _ => Err(SyscallError::InvalidOp), + } +} diff --git a/kernel/nucleus/src/api/endpoint.rs b/kernel/nucleus/src/api/endpoint.rs new file mode 100644 index 000000000..0e14c9e8c --- /dev/null +++ b/kernel/nucleus/src/api/endpoint.rs @@ -0,0 +1,495 @@ +// Endpoint capability operations +// +// Endpoints enable synchronous call/return IPC with direct domain switch. +// Unlike Notifications (fire-and-forget) or EventCounts (streaming), +// Endpoints are for request/response patterns. + +use crate::{key::Cap, syscall::protected_call4}; + +// ================================================== +// == Public user interface, usable from userspace == +// ================================================== + +/// Endpoint capability for synchronous IPC +/// +/// Two "views" of the same endpoint: +/// - Client holds EndpointCap (can Call/Send) +/// - Server holds EndpointCap with Recv rights (can Recv/Reply) +/// +/// Badges identify which client is calling (granted during cap derivation) +/// +/// With explicit Reply objects: +/// - recv() returns (badge, msg, ReplyCap) +/// - reply is done via ReplyCap::send(), not endpoint method +/// - reply_recv() takes a ReplyCap to consume +pub struct EndpointCap { + cap: Cap, +} + +impl EndpointCap { + // ═══════════════════════════════════════════════════════════════════ + // CLIENT OPERATIONS + // ═══════════════════════════════════════════════════════════════════ + + /// Call: send message and block waiting for reply + /// + /// This is the primary client→server operation. + /// Performs direct domain switch to receiver (fast path). + /// + /// Returns the reply message (label + data + optional cap) + pub fn call(&self, msg: &Message) -> Result { + let (ret0, ret1, ret2, ret3, ret4, ret5, ret6) = unsafe { + syscall_ipc( + self.cap.slot as u64, + EndpointOp::Call as u64, + msg.label, + msg.data[0], + msg.data[1], + msg.data[2], + msg.data[3], + msg.data[4], + msg.cap.map(|s| s as u64).unwrap_or(u64::MAX), + ) + }; + + if ret0 == 0 { + Ok(Message { + label: ret1, + data: [ret2, ret3, ret4, ret5, 0], + cap: if ret6 != u64::MAX { + Some(ret6 as CapSlot) + } else { + None + }, + }) + } else { + Err(IpcError::from_code(ret0)) + } + } + + /// Send: non-blocking send (no reply expected) + /// + /// If receiver is waiting → message delivered, returns Ok + /// If no receiver → message dropped, returns Err(WouldBlock) + pub fn send(&self, msg: &Message) -> Result<(), IpcError> { + let ret = unsafe { + protected_call4( + self.cap.slot as u64, + EndpointOp::Send as u64, + msg.label, + msg.data[0], + msg.data[1], + msg.data[2], + ) + }; + IpcError::from_code(ret.0) + } + + // ═══════════════════════════════════════════════════════════════════ + // SERVER OPERATIONS + // ═══════════════════════════════════════════════════════════════════ + + /// Recv: block waiting for incoming Call + /// + /// Returns (sender_badge, message, reply_cap) + /// Badge identifies which client called (set during cap derivation) + /// + /// The ReplyCap MUST be used to reply - it's consumed on use. + /// Dropping it without replying will send an error to the caller. + pub fn recv(&self, reply_slot: CapSlot) -> Result<(u64, Message, ReplyCap), IpcError> { + let (ret0, badge, label, d0, d1, d2, d3, d4, cap_slot) = unsafe { + syscall_ipc_recv( + self.cap.slot as u64, + EndpointOp::Recv as u64, + reply_slot as u64, // Where kernel places ReplyCap + ) + }; + + if ret0 == 0 { + let msg = Message { + label, + data: [d0, d1, d2, d3, d4], + cap: if cap_slot != u64::MAX { + Some(cap_slot as CapSlot) + } else { + None + }, + }; + + // Kernel placed ReplyCap in reply_slot + let reply_cap = ReplyCap { + cap: Cap::new(reply_slot), + }; + + Ok((badge, msg, reply_cap)) + } else { + Err(IpcError::from_code(ret0)) + } + } + + /// ReplyRecv: consume reply cap, send reply, AND wait for next message + /// + /// This is the server fast-path: one syscall does Reply + Recv. + /// + /// Takes the ReplyCap to consume (must reply to previous caller) + /// Returns new (badge, msg, reply_cap) for next caller + pub fn reply_recv( + &self, + reply_cap: ReplyCap, + reply_msg: &Message, + next_reply_slot: CapSlot, + ) -> Result<(u64, Message, ReplyCap), IpcError> { + let (ret0, badge, label, d0, d1, d2, d3, d4, cap_slot) = unsafe { + syscall_ipc_reply_recv( + self.cap.slot as u64, + EndpointOp::ReplyRecv as u64, + reply_cap.cap.slot as u64, // Reply cap to consume + next_reply_slot as u64, // Where to place next ReplyCap + reply_msg.label, + reply_msg.data[0], + reply_msg.data[1], + reply_msg.data[2], + reply_msg.data[3], + ) + }; + + // reply_cap consumed by kernel + core::mem::forget(reply_cap); + + if ret0 == 0 { + let msg = Message { + label, + data: [d0, d1, d2, d3, d4], + cap: if cap_slot != u64::MAX { + Some(cap_slot as CapSlot) + } else { + None + }, + }; + + let next_reply = ReplyCap { + cap: Cap::new(next_reply_slot), + }; + + Ok((badge, msg, next_reply)) + } else { + Err(IpcError::from_code(ret0)) + } + } + + /// Forward: pass this call to another endpoint (proxy pattern) + /// + /// Useful for: capability-filtered proxies, load balancers, etc. + /// The forwarded call appears to come from us (our badge) + pub fn forward(&self, target: &EndpointCap, msg: &Message) -> Result<(), IpcError> { + let ret = unsafe { + protected_call4( + self.cap.slot as u64, + EndpointOp::Forward as u64, + target.cap.slot as u64, + msg.label, + msg.data[0], + msg.data[1], + ) + }; + IpcError::from_code(ret.0) + } +} + +impl EndpointCap { + /// Derive a client capability with a specific badge + /// + /// The badge is returned to the server on recv(), identifying the caller. + /// This is how servers distinguish between clients. + pub fn derive_client(&self, badge: u64, dest_slot: CapSlot) -> Result { + let ret = unsafe { + syscall4( + CAPTBL_SELF, // Support deriving directly into a client keytable? + KeyTableOp::CopyDerive, + self.cap.slot as u64, + dest_slot as u64, + Rights::CALL.bits() as u64, // Client can only Call, not Recv + badge, + ) + }; + + if ret == 0 { + Ok(EndpointCap { + cap: Cap::new(dest_slot), + }) + } else { + Err(Error::from_code(ret)) + } + } +} + +// Client Domain Server Domain +// ───────────── ───────────── +// │ │ +// │ ep.call(msg) │ ep.recv() +// │ │ +// ▼ ▼ +// ┌─────────┐ ┌─────────┐ +// │ BLOCKED │ │ BLOCKED │ +// │ waiting │ │ waiting │ +// │ for │ │ for │ +// │ reply │ │ sender │ +// └────┬────┘ └────┬────┘ +// │ │ +// │ ┌───────────────────┐ │ +// │ │ KERNEL SWITCH │ │ +// ├───────►│ │◄────────────┤ +// │ │ 1. Copy msg regs │ │ +// │ │ 2. Set badge │ │ +// │ │ 3. Switch domain │ │ +// │ └───────────────────┘ │ +// │ │ +// │ ▼ +// │ (running) +// │ process msg +// │ │ +// │ ┌───────────────────┐ │ +// │ │ KERNEL SWITCH │ │ +// │◄───────│ │◄────────────┤ +// │ │ 1. Copy reply │ │ ep.reply(response) +// │ │ 2. Switch back │ │ +// │ └───────────────────┘ │ +// ▼ +// (running) +// got reply + +// ============================================== +// == Kernel space object and syscall handling == +// ============================================== + +#[repr(u8)] +pub enum EndpointOp { + /// Send message and wait for reply (client operation) + /// Blocks until server receives, processes, and replies + Call = 0, + + /// Send message without waiting (non-blocking) + /// If no receiver waiting, message is dropped (returns error) + Send = 1, + + /// Wait for incoming message (server operation) + /// Blocks until a sender calls + Recv = 2, + + /// Reply to caller and wait for next message (server fast-path) + /// Combines Reply + Recv in single syscall (seL4 optimization) + ReplyRecv = 3, + + /// Reply to caller without waiting for next + Reply = 4, + + /// Forward call to another endpoint (proxy pattern) + /// Transfers the reply capability to the new endpoint + Forward = 5, +} + +/// Message passed through endpoints +/// Fits in registers for fast IPC (~200-500 cycles) +#[repr(C)] +pub struct Message { + /// Message label/opcode - receiver dispatches on this + pub label: u64, + + /// 5 general-purpose data words + pub data: [u64; 5], + + /// Optional capability to transfer (None = no transfer) + /// Sender's cap is moved (not copied) to receiver + pub cap: Option, +} + +impl Message { + pub const fn new(label: u64) -> Self { + Self { + label, + data: [0; 5], + cap: None, + } + } + + pub fn with_data(label: u64, d0: u64, d1: u64, d2: u64, d3: u64, d4: u64) -> Self { + Self { + label, + data: [d0, d1, d2, d3, d4], + cap: None, + } + } + + pub fn with_cap(mut self, cap_slot: CapSlot) -> Self { + self.cap = Some(cap_slot); + self + } +} + +/// Kernel object +struct Endpoint { + /// Domain that can receive on this endpoint (None = anyone) + receiver: Option, + + /// Current state + state: EndpointState, + + /// Waiting senders (if state == Recving, this is empty) + /// Waiting receivers (if state == Sending, this is empty) + wait_queue: WaitQueue, + + /// Message buffer (kernel memory) + msg_regs: [u64; 6], + + /// Badge of sender (filled when message delivered) + sender_badge: u64, + + /// Cap transfer slot + transfer_cap: Option, +} + +enum EndpointState { + Idle, + /// Someone is blocked sending + Sending, + /// Someone is blocked receiving + Recving, +} + +impl Endpoint { + fn handle_call( + &mut self, + caller: &mut Domain, + msg: &[u64; 6], + cap_slot: Option, + badge: u64, + ) -> SyscallResult { + match self.state { + EndpointState::Recving => { + // Receiver waiting! Fast path - direct switch + let receiver = self.wait_queue.pop_front().unwrap(); + + // Copy message to receiver + self.msg_regs = *msg; + self.sender_badge = badge; + self.transfer_cap = cap_slot; + + // Block caller waiting for reply + caller.state = DomainState::Blocked; + caller.block_reason = BlockReason::Endpoint; + + // Wake receiver + receiver.state = DomainState::Runnable; + + // Switch to receiver (donate remaining time) + switch_to(receiver); + + Ok(0) + } + + EndpointState::Idle | EndpointState::Sending => { + // No receiver - block caller + caller.state = DomainState::Blocked; + caller.block_reason = BlockReason::Endpoint; + + self.wait_queue.push_back(caller.id); + self.state = EndpointState::Sending; + + // Store message for when receiver arrives + self.msg_regs = *msg; + self.sender_badge = badge; + self.transfer_cap = cap_slot; + + // Schedule someone else + schedule_next(); + + Ok(0) + } + } + } + + fn handle_recv(&mut self, receiver: &mut Domain, reply_dest_slot: CapSlot) -> SyscallResult { + match self.state { + EndpointState::Sending => { + let sender_info = self.wait_queue.pop_front().unwrap(); + + // Create Reply object for this call + let reply = Reply { + caller: sender_info.domain_id, + state: ReplyState::Pending, + }; + + // Allocate kernel memory for Reply object - FIXME: kernel memory allocation! + // and place capability in receiver's cspace + let reply_cap = kernel_alloc_reply(reply)?; + receiver.cspace.insert(reply_dest_slot, reply_cap)?; + + // Copy message to receiver + receiver.regs.x0 = 0; // Success + receiver.regs.x1 = sender_info.badge; + receiver.regs.x2 = self.msg_regs[0]; // label + receiver.regs.x3 = self.msg_regs[1]; + receiver.regs.x4 = self.msg_regs[2]; + receiver.regs.x5 = self.msg_regs[3]; + receiver.regs.x6 = self.msg_regs[4]; + receiver.regs.x7 = self.msg_regs[5]; + // x8 = transferred cap slot (if any) + + if self.wait_queue.is_empty() { + self.state = EndpointState::Idle; + } + + Ok(0) + } + + EndpointState::Idle | EndpointState::Recving => { + // Block receiver + receiver.state = DomainState::Blocked; + receiver.block_reason = BlockReason::Endpoint; + receiver.blocked_data = reply_dest_slot as u64; // Remember where to put reply cap + + self.wait_queue.push_back(receiver.id); + self.state = EndpointState::Recving; + + schedule_next(); + Ok(0) + } + } + } + + fn handle_reply_recv( + &mut self, + server: &mut Domain, + reply_cap_slot: CapSlot, + next_reply_slot: CapSlot, + reply_msg: &[u64; 6], + ) -> SyscallResult { + // 1. Send reply via the provided reply cap + let reply_cap = server.cspace.take(reply_cap_slot)?; + let reply = reply_cap.as_reply()?; + reply.handle_send(reply_msg, None)?; + + // Reply object is consumed, free kernel memory + kernel_free_reply(reply); + + // 2. Receive next message + self.handle_recv(server, next_reply_slot) + } +} + +// ===================== +// == Syscall handler == +// ===================== + +pub fn invoke(cap: &Cap, op: u32, arg0: u64, arg1: u64) -> SyscallResult { + let ep = cap.as_endpoint()?; + match op { + EndpointOp::Call => ep.handle_call(), + EndpointOp::Send => ep.handle_send(), + EndpointOp::Recv => ep.handle_recv(), + EndpointOp::Reply => { /* invoke the reply_cap */ } + EndpointOp::ReplyRecv => ep.handle_replyrecv(), + EndpointOp::Forward => ep.handle_forward(), + _ => Err(SyscallError::InvalidOp), + } +} diff --git a/kernel/nucleus/src/api/event_count.rs b/kernel/nucleus/src/api/event_count.rs new file mode 100644 index 000000000..f27017e31 --- /dev/null +++ b/kernel/nucleus/src/api/event_count.rs @@ -0,0 +1,103 @@ +// Reed-Kanodia event counts + +// ================================================== +// == Public user interface, usable from userspace == +// ================================================== + +/// Event count capability - monotonic counter for exact event tracking. +/// Best for: streaming, flow control, producer-consumer coordination. +/// +/// Unlike notifications, every advance() is counted - no coalescing. +/// This lets consumers know exactly how far behind they are. +pub struct EventCountKey { + key: Key, +} + +impl EventCountCap { + /// Advance: atomically increment counter by delta. + /// Returns new value. Never blocks. ~30 cycles. + #[inline] + pub fn advance(&self, delta: u64) -> u64 { + protected_call1(self.key.slot as u64, EventOp::Advance as u64, delta) + } + + /// Await: block until value >= target. + /// Returns current value (may be > target if producer is fast). + #[inline] + pub fn await_ge(&self, target: u64) -> u64 { + protected_call1(self.key.slot as u64, EventOp::Await as u64, target) + } + + /// Read: get current value without blocking. + #[inline] + pub fn read(&self) -> u64 { + protected_call0(self.key.slot as u64, EventOp::Read) + } +} + +/// Helper: tracks consumer position for a single reader +pub struct EventCountReader { + ec: EventCountKey, + last_seen: u64, +} + +impl EventCountReader { + pub fn new(ec: EventCountKey) -> Self { + let initial = ec.read(); + Self { + ec, + last_seen: initial, + } + } + + /// Wait for next event(s), returns count since last wait + pub fn wait_next(&mut self) -> u64 { + let target = self.last_seen + 1; + let current = self.ec.await_ge(target); + let delta = current - self.last_seen; + self.last_seen = current; + delta + } + + /// Check how many events pending without blocking + pub fn pending(&self) -> u64 { + self.ec.read() - self.last_seen + } +} + +// ============================================== +// == Kernel space object and syscall handling == +// ============================================== + +// Kernel object +struct EventCount { + value: u64, // Monotonically increasing counter + waiters: WaitQueue, // Domains waiting for value >= target +} + +#[repr(u8)] +enum EventCountOp { + Advance = 0, + Await = 1, + Read = 2, +} + +// ===================== +// == Syscall handler == +// ===================== + +pub fn invoke(cap: &Cap, op: u32, arg0: u64) -> SyscallResult { + let ec = cap.as_event_count()?; + match op { + EventOp::Advance => { + Ok(ec.advance(arg0)) // atomic ADD, returns new value + } + EventOp::Await => { + Ok(ec.await_ge(arg0)) // blocks until >= arg0 + } + EventOp::Read => { + Ok(ec.read()) // non-blocking read + } + _ => Err(SyscallError::InvalidOp), + } +} diff --git a/kernel/nucleus/src/api/key.rs b/kernel/nucleus/src/api/key.rs new file mode 100644 index 000000000..73ddfe3a2 --- /dev/null +++ b/kernel/nucleus/src/api/key.rs @@ -0,0 +1,26 @@ +use core::marker::PhantomData; + +// ================================================== +// == Public user interface, usable from userspace == +// ================================================== + +/// Capability slot index - typed for safety +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +#[repr(transparent)] +pub struct Key { + slot: u32, + _marker: PhantomData, +} + +impl Key { + pub const fn new(slot: u32) -> Self { + Self { + slot, + _marker: PhantomData, + } + } + + pub const fn slot(&self) -> u32 { + self.slot + } +} diff --git a/kernel/nucleus/src/api/key_table.rs b/kernel/nucleus/src/api/key_table.rs new file mode 100644 index 000000000..8cd4bd1dd --- /dev/null +++ b/kernel/nucleus/src/api/key_table.rs @@ -0,0 +1,140 @@ +// ================================================== +// == Public user interface, usable from userspace == +// ================================================== + +use crate::{ + SyscallError, + api::{protected_call1, protected_call4}, +}; + +pub struct KeyTableKey { + key: Key, +} + +// Userspace KeyManager must track parent→child relationships, +// kernel only manages flat key tables. + +impl KeyTableKey { + // This naturally supports cross-domain derivation: + // "Create a read-only view of my buffer in their cspace" + // derive(&my_captbl, buffer_slot, &their_captbl, their_slot, Rights::READ)?; + /// Copy with derivation in single syscall + pub fn copy_derive( + &self, + src_slot: u32, + dst_captbl: &KeyTableKey, // Could be same or different! + dst_slot: u32, + rights: Rights, + ) -> Result<()> { + protected_call4( + self.key.slot(), + KeyTableOp::CopyDerive, + src_slot, + dst_captbl.slot(), + dst_slot, + rights.bits(), + ) + } + + // fn activate(&self, slot: u32, object: KernelObject) -> Result<()> { + // let captbl = self.get_captbl_mut()?; + // // SAFETY: User specifies slot, but kernel validates + // if slot >= captbl.len() { + // return Err(Error::SlotOutOfRange); + // } + // if captbl.slots[slot].is_valid() { + // return Err(Error::SlotOccupied); // User's bookkeeping was wrong + // } + // // Kernel creates the cap - user never touches this + // captbl.slots[slot] = Cap::new(object); + // Ok(()) + // } + + fn r#move() {} + + fn delete(&mut self, slot: u32) -> Result<()> { + // TODO: Must invoke on self-captbl cap + protected_call1(self.key.slot(), KeyTableOp::Delete, slot) + } + + // Revoke all children of cap in slot + fn revoke(&self, captbl: &CaptblCap, slot: u32) -> Result<()> { + protected_call1(self.key.slot(), KeyTableOp::Revoke, slot) + } + + // User code to copy cap to another domain (if you have their captbl cap): + fn grant_to(my_slot: u32, their_captbl: &CaptblCap, their_slot: u32) -> Result<()> { + protected_call3( + self.key.slot(), + KeyTableOp::CopyDerive, + my_slot, + their_captbl.slot(), + their_slot, + same_rights, + ) + } +} + +// ============================================== +// == Kernel space object and syscall handling == +// ============================================== + +#[repr(u8)] +enum KeyTableOp { + CopyDerive = 0, // copy cap between slots or create derived cap with reduced rights + Move = 1, // move cap between slots + Delete = 2, // delete cap at slot + Revoke = 4, // revoke all children of cap +} + +impl KeyTableOp { + fn try_from(op: u32) -> Result { + match op { + 0 => Ok(KeyTableOp::CopyDerive), + 1 => Ok(KeyTableOp::Move), + 2 => Ok(KeyTableOp::Delete), + 3 => Ok(KeyTableOp::Revoke), + _ => Err(SyscallError::InvalidOp), + } + } +} + +struct KeyTable { + slots: [u64; 256], + len: usize, +} + +impl KeyTable { + fn delete(&self, slot: u32) -> Result<()> { + if slot >= self.len() { + return Err(Error::SlotOutOfRange); + } + + if !self.slots[slot].is_valid() { + return Err(Error::SlotEmpty); + } + + let cap = self.slots[slot].take(); + cap.destroy()?; + + Ok(()) + } +} + +// ===================== +// == Syscall handler == +// ===================== + +pub fn invoke(key: &Key, op: u32, args: &[u64]) -> SyscallResult { + let captbl = key.as_keytable()?; + match KeyTableOp::try_from(op)? { + KeyTableOp::CopyDerive => { + let (src, dst_captbl, dst_slot) = (args[0], args[1], args[2]); + // Copy cap from this captbl[src] to dst_captbl[dst_slot] + //... + } + KeyTableOp::Move => {} + KeyTableOp::Delete => {} + KeyTableOp::Revoke => {} + } +} diff --git a/kernel/nucleus/src/api/mod.rs b/kernel/nucleus/src/api/mod.rs new file mode 100644 index 000000000..38e61fcd4 --- /dev/null +++ b/kernel/nucleus/src/api/mod.rs @@ -0,0 +1,20 @@ +// TODO: move these api decls/impls to libraries in libs/, re-export only user part through +// TODO: libsyscall or something like that - usable from userspace. + +mod buffer; +mod debug_console; +mod domain; +mod endpoint; +mod event_count; +mod key; +mod key_table; +mod notification; +mod reply; +mod syscall; +mod time; +mod untyped; + +pub use syscall::{ + protected_call0, protected_call1, protected_call2, protected_call3, protected_call4, + protected_call5, protected_call6, +}; diff --git a/kernel/nucleus/src/api/notification.rs b/kernel/nucleus/src/api/notification.rs new file mode 100644 index 000000000..8d1663079 --- /dev/null +++ b/kernel/nucleus/src/api/notification.rs @@ -0,0 +1,81 @@ +use crate::syscall::{protected_call0, protected_call1}; + +// ================================================== +// == Public user interface, usable from userspace == +// ================================================== + +/// Notification capability - bitmap-based async signaling. +/// Best for: IRQs, completion events, wakeups. +pub struct NotificationKey { + key: Key, +} + +impl NotificationKey { + /// Signal: atomic OR into bitmap (always non-blocking) + /// Multiple signals to same bit coalesce. + /// ~30 cycles, no domain switch needed + #[inline] + pub fn signal(&self, bits: u64) -> Result<()> { + protected_call1(self.slot, NotifyOp::Signal as u32, bits)?; + Ok(()) + } + + /// Wait: block until any bit set, returns + clears ALL bits + #[inline] + pub fn wait(&self) -> Result { + protected_call0(self.slot, NotifyOp::Wait as u32) + } + + /// Poll: non-blocking check, returns + clears bits + #[inline] + pub fn poll(&self) -> u64 { + protected_call0(self.cap.slot as u64, NotifyOp::Poll as u32) + } +} + +// ============================================== +// == Kernel space object and syscall handling == +// ============================================== + +struct Notification { + state: u64, // Bitmap + waiters: WaitQueue, // Blocked domains + bound: Option, // Optional bound domain for fast wakeup +} + +#[repr(u8)] +enum NotifyOp { + Signal = 0, + Wait = 1, + Poll = 2, +} + +impl Notification { + fn signal(&mut self, bits: u64) {} + fn wait() { + // if bits are already set, clear and immediately return + // otherwise block the domain.. + } + fn poll() {} +} + +// ===================== +// == Syscall handler == +// ===================== + +pub fn invoke(cap: &Cap, op: u32, arg0: u64) -> SyscallResult { + let notify = cap.as_notification()?; + match op { + NotifyOp::Signal => { + notify.signal(arg0); // atomic OR + Ok(0) + } + NotifyOp::Wait => { + Ok(notify.wait()) // blocks, returns + clears + } + NotifyOp::Poll => { + Ok(notify.poll()) // non-blocking + } + _ => Err(SyscallError::InvalidOp), + } +} diff --git a/kernel/nucleus/src/api/reply.rs b/kernel/nucleus/src/api/reply.rs new file mode 100644 index 000000000..c1dc3ce70 --- /dev/null +++ b/kernel/nucleus/src/api/reply.rs @@ -0,0 +1,194 @@ +//! Endpoint IPC Reply object + +// ================================================== +// == Public user interface, usable from userspace == +// ================================================== + +/// Reply capability - one-shot reply to a blocked caller +/// +/// Created by kernel when a Call arrives, consumed when reply is sent. +/// This is a LINEAR type - must be used exactly once (or explicitly dropped). +/// +/// Key insight from seL4 MCS: making reply explicit enables: +/// - Async reply (store reply cap, reply later) +/// - Delegation (pass reply cap to helper domain) +/// - Multiple outstanding calls (each has own reply cap) +pub struct ReplyKey { + key: Key, +} + +impl ReplyKey { + /// Send reply to the blocked caller + /// + /// Consumes self - reply cap is one-shot! + /// After this, the caller is unblocked with the response. + pub fn send(self, msg: &Message) -> Result<(), IpcError> { + let ret = unsafe { + protected_call4( + self.key.slot as u64, + ReplyOp::Send as u64, + msg.label, + msg.data[0], + msg.data[1], + msg.data[2], + ) + }; + + // Consumed - don't run Drop (kernel already invalidated) + core::mem::forget(self); + + IpcError::from_code(ret.0) + } + + /// Send reply with capability transfer + pub fn send_with_key(self, msg: &Message, key: KeySlot) -> Result<(), IpcError> { + let ret = unsafe { + syscall_ipc_reply( + self.cap.slot as u64, + ReplyOp::SendWithCap as u64, + msg.label, + msg.data[0], + msg.data[1], + msg.data[2], + msg.data[3], + msg.data[4], + cap as u64, + ) + }; + + // Consumed - don't run Drop (kernel already invalidated) + core::mem::forget(self); + + IpcError::from_code(ret.0) + } +} + +/// Dropping a ReplyKey without sending is an ERROR for the caller. +/// The caller remains blocked forever (or until timeout/cancellation). +/// +/// In debug builds, we panic. In release, we send an error reply. +impl Drop for ReplyKey { + fn drop(&mut self) { + // Reply cap dropped without sending - this is usually a bug! + // Send error reply to unblock caller + #[cfg(debug_assertions)] + panic!("ReplyKey dropped without sending reply!"); + + #[cfg(not(debug_assertions))] + unsafe { + protected_call1( + self.cap.slot as u64, + ReplyOp::SendError as u64, + IpcError::ReplyDropped as u64, + ); + } + } +} + +// ============================================== +// == Kernel space object and syscall handling == +// ============================================== + +/// Reply kernel object +struct Reply { + /// Domain waiting for this reply + caller: DomainId, + + /// State of this reply object + state: ReplyState, +} + +#[derive(Clone, Copy, PartialEq)] +enum ReplyState { + /// Caller is blocked waiting + Pending, + /// Reply was sent, object is consumed + Used, + /// Caller cancelled or timed out + Cancelled, +} + +#[repr(u8)] +enum ReplyOp { + Send = 0, // Send reply message + SendWithCap = 1, // Send reply with cap transfer + SendError = 2, // Send error (used by Drop) +} + +impl Reply { + fn handle_send(&mut self, msg: &[u64; 6], cap_slot: Option) -> SyscallResult { + match self.state { + ReplyState::Pending => { + let caller = get_domain_mut(self.caller); + + // Copy reply to caller's registers + caller.regs.x0 = 0; // Success + caller.regs.x1 = msg[0]; // label + caller.regs.x2 = msg[1]; + caller.regs.x3 = msg[2]; + caller.regs.x4 = msg[3]; + caller.regs.x5 = msg[4]; + caller.regs.x6 = msg[5]; + + // Transfer cap if present + if let Some(slot) = cap_slot { + let cap = current_domain().cspace.take(slot)?; + caller.cspace.insert(RECEIVED_CAP_SLOT, cap)?; + caller.regs.x7 = RECEIVED_CAP_SLOT as u64; + } else { + caller.regs.x7 = u64::MAX; + } + + // Wake caller + caller.state = DomainState::Runnable; + + // Mark reply object as used (one-shot) + self.state = ReplyState::Used; + + Ok(0) + } + + ReplyState::Used => Err(SyscallError::ReplyAlreadyUsed), + + ReplyState::Cancelled => { + // Caller gave up - just consume the reply cap + self.state = ReplyState::Used; + Ok(0) // Not an error, just no-op + } + } + } +} + +// CLIENT DOMAIN KERNEL SERVER DOMAIN +// ───────────── ────── ───────────── +// +// CSpace: Reply Pool: CSpace: +// ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ +// │ ... │ │ Reply #0 │ │ ... │ +// │ ep_cap ──────┼──┐ │ Reply #1 ◄───┼─────────┼─ reply_slot │ +// │ ... │ │ │ Reply #2 │ │ ... │ +// └──────────────┘ │ │ ... │ └──────────────┘ +// │ └──────────────┘ +// │ ▲ +// │ │ +// ▼ │ +// ┌────────────┐ │ +// │ Endpoint │ │ +// │ │ kernel creates +// │ state: │ Reply object +// │ Idle │ on Call arrival +// │ │ +// │ waiters: │ +// │ (empty) │ +// └────────────┘ + +// ===================== +// == Syscall handler == +// ===================== + +pub fn invoke(cap: &CapEntry, op: u32, arg0: u64, arg1: u64) -> SyscallResult { + match op { + ReplyOp::Send => handle_send(), + _ => Err(SyscallError::InvalidOp), + } +} diff --git a/kernel/nucleus/src/api/syscall.rs b/kernel/nucleus/src/api/syscall.rs new file mode 100644 index 000000000..71af17952 --- /dev/null +++ b/kernel/nucleus/src/api/syscall.rs @@ -0,0 +1,128 @@ +// Syscall ABI: +// ┌─────────────────────────────────────────────────────────────────────────┐ +// │ CAPTBL COPY (cross-domain grant): │ +// │ ┌─────────────────────────────────────────────────────────────────┐ │ +// │ │ x0 = src_captbl x3 = dst_captbl │ │ +// │ │ x1 = COPY op x4 = dst_slot │ │ +// │ │ x2 = src_slot x5 = rights_mask ← derive+copy in one! │ │ +// │ └─────────────────────────────────────────────────────────────────┘ │ +// │ │ +// │ BUFFER MAP (with full control): │ +// │ ┌─────────────────────────────────────────────────────────────────┐ │ +// │ │ x0 = buffer_cap x3 = size │ │ +// │ │ x1 = MAP op x4 = offset ← map partial buffer │ │ +// │ │ x2 = virt_addr x5 = flags ← cache policy, etc. │ │ +// │ └─────────────────────────────────────────────────────────────────┘ │ +// │ │ +// │ ENDPOINT CALL (with inline payload): │ +// │ ┌─────────────────────────────────────────────────────────────────┐ │ +// │ │ x0 = endpoint_cap x3 = msg_word_1 │ │ +// │ │ x1 = CALL op x4 = msg_word_2 │ │ +// │ │ x2 = msg_word_0 x5 = msg_word_3 ← 4 words inline! │ │ +// │ └─────────────────────────────────────────────────────────────────┘ │ +// │ │ +// │ UNTYPED RETYPE (batch creation): │ +// │ ┌─────────────────────────────────────────────────────────────────┐ │ +// │ │ x0 = untyped_cap x3 = dest_captbl │ │ +// │ │ x1 = RETYPE op x4 = dest_slot_start │ │ +// │ │ x2 = obj_type x5 = count ← create N objects at once! │ │ +// │ └─────────────────────────────────────────────────────────────────┘ │ +// └─────────────────────────────────────────────────────────────────────────┘ + +/// Single syscall ABI +/// +/// Entry: SVC #0 +/// +/// Arguments: +/// x0 = capability slot +/// x1 = operation code +/// x2-x7 = operation arguments (6 args!) +/// x9-x15 are caller-saved, we don't use them +/// +/// Returns: +/// x0 = error code (0 = success) +/// x1 = return value 0 +/// x2 = return value 1 (if needed) +#[inline(always)] +pub unsafe fn protected_call6( + cap: u32, + op: u32, + a0: u64, + a1: u64, + a2: u64, + a3: u64, + a4: u64, + a5: u64, +) -> (u64, u64, u64) { + let r0: u64; + let r1: u64; + let r2: u64; + + core::arch::asm!( + "svc #0", + inlateout("x0") cap as u64 => r0, + inlateout("x1") op as u64 => r1, + inlateout("x2") a0 => r2, + in("x3") a1, + in("x4") a2, + in("x5") a3, + in("x6") a4, + in("x7") a5, + options(nostack), + ); + + (r0, r1, r2) +} + +// Most operations don't need all 6 args - provide convenience wrappers +// TODO: don't waste registers to zero them out for a less-than-6-args versions? Might be risky... + +/// 0-arg invoke (cap + op only) +#[inline(always)] +pub fn protected_call0(cap: u32, op: u32) -> Result { + let (err, val, _) = unsafe { protected_call6(cap, op, 0, 0, 0, 0, 0, 0) }; + if err == 0 { Ok(val) } else { Err(Error(err)) } +} + +/// 1-arg invoke +#[inline(always)] +pub fn protected_call1(cap: u32, op: u32, a0: u64) -> Result { + let (err, val, _) = unsafe { protected_call6(cap, op, a0, 0, 0, 0, 0, 0) }; + if err == 0 { Ok(val) } else { Err(Error(err)) } +} + +/// 2-arg invoke +#[inline(always)] +pub fn protected_call2(cap: u32, op: u32, a0: u64, a1: u64) -> Result { + let (err, val, _) = unsafe { protected_call6(cap, op, a0, a1, 0, 0, 0, 0) }; + if err == 0 { Ok(val) } else { Err(Error(err)) } +} + +/// 3-arg invoke +#[inline(always)] +pub fn protected_call3(cap: u32, op: u32, a0: u64, a1: u64, a2: u64) -> Result { + let (err, val, _) = unsafe { protected_call6(cap, op, a0, a1, a2, 0, 0, 0) }; + if err == 0 { Ok(val) } else { Err(Error(err)) } +} + +/// 4-arg invoke +#[inline(always)] +pub fn protected_call4(cap: u32, op: u32, a0: u64, a1: u64, a2: u64, a3: u64) -> Result { + let (err, val, _) = unsafe { protected_call6(cap, op, a0, a1, a2, a3, 0, 0) }; + if err == 0 { Ok(val) } else { Err(Error(err)) } +} + +/// 5-arg invoke +#[inline(always)] +pub fn protected_call5( + cap: u32, + op: u32, + a0: u64, + a1: u64, + a2: u64, + a3: u64, + a4: u64, +) -> Result { + let (err, val, _) = unsafe { protected_call6(cap, op, a0, a1, a2, a3, a4, 0) }; + if err == 0 { Ok(val) } else { Err(Error(err)) } +} diff --git a/kernel/nucleus/src/api/time.rs b/kernel/nucleus/src/api/time.rs new file mode 100644 index 000000000..8ef318a87 --- /dev/null +++ b/kernel/nucleus/src/api/time.rs @@ -0,0 +1,105 @@ +// ================================================== +// == Public user interface, usable from userspace == +// ================================================== + +/// Time capability — represents a slice of CPU time +pub struct TimeKey { + key: Key

pALRF?4rLH%do@woQ_MPqMH zN|Zk{n9Q>O7OiM0x5a4VeAgXwNBBB}2Wu~LbX=%OPor`sOvJ>@q|;UU6R`(zGc86UlY<+kdG`}s zwwO7Qtc(eoK?GsQ8;pa?1ovTqh>E^0TDQBwWY_bOa|O6YJd_?_f+wbz}c2|ELtgDH8P_0%SD%a4495mkH2zd#pquGwxM?d4o#3gH%aHiYSzyAGTk zxjgP=oPJn^imw!?%vB+xh^E&@Mq9e3>e_%w#+Ja(8*iqrE_F_-K#^j{yTFTh?4b&Z z7Snxs-P!>RmIhT1lZ);4)8erBWhqwYLgdfpZY{$fXqcDqWD&vu>KVd3{5Nxy~-|*SA;A36igAXmf@q z2!l%EkZBR0&(rr2G$Aa&nMSI|ZDZMJwmR6ZKmOA5AKaa@_1 z_@q#Ae=`+R4YM?CtHQgaR{@1}Hg&bZs755b_tI=zz3Nl*5132BDH>{iy@wJ{n#(-c zKYwZ~ru{Nm&CoOSm-BtGjS`4(mhgPqZsuk>3hM8c17|}Wqdkxe8f5&U5mKtFv5Pv? zDee)9Qh0@wobMr+X6{*FzZU-B3z2yQd6=NiH-@}OEHl`5%WneVU^#wy6AOQ)x^xh; zFCo$`&%o(RVrl#s2Y1hQP%`lpku~*A#@|><>3A9bUG|~|jh16T)`cN6c`OGn2Et_Y z*=!#m6scqitL52(c`J~yw`}i+Ev+yQ%oSz7zS%C(F0>zO*AjtfaNS^`$=xE78s5nQ zC7L~WR_X>H18Sj9?!U4~fS0CoTfNun^6XIhY*4=H>%_7Uvg)TZfE!J|hu$qm_vT@e z_Sl8IB()2Bj@lT2Ini3~@5vGeztr2@BV{vo%VUeTj1j+tV{2dAtnOrhDP2B8Y#soV zJ0)Qts=u3%(YzoDjf1gqm(D0|Rk@;}4rQ!ouim(^NYc|9k{Yv`QJ|r#6l-8yH9Tlz zAzpKF_veEw{Y++3{2`~vH~5MeU#=V92P6h~6Ln>oOki#&W z`B^xdJ~o6gJnfLfyuVHlwQRs-Y+Q_O7b&YpS-d;nSQ(#{ponwmk(;m(N;M<2b%$YDvY z2AS9fr^yHL+9}=jshWsQA+?5TN`x@@l=fm-ZG|p&lBbT7Zp9o<_LgM;dl)y~%{HKG zGa5YX(N<*2Sjqr`KVsMAY?4Z}A@jh}P%5ko+)lC{7t#{GO6` z7Pfqos9BPQPWz7kqadJb5#N*n)xSm<|9xY#e@SGx|Jy_+aRRxY2?|u#8T#axJnp;2 zVj%8B*l{_sl&xK|X#(%EK!?N{H9X@Cl{@|K5rey{@Q(Smlg2{r&T;G=UxeL#o}O(# zmr;EY2m2edx^BiN6bCuJ>d(R#Cym#8gw^re-ok=AMPkpKDgV>eQ*WuXtsmwn*GXahX{YRBK^FKP+|NkkL;b!{Z#WMf65C7L)#Ms!G{&S)E)jtc($97Lu z@5JsR&S9CAfh5l||(13q$#>7q--WmoWZ7@fc`q&T?o zTmtgm=k^ztF7}760>lHp-tVkha<&6=`Y1gkmySu_ukYTx-Z49xGGr83sDGr3!Ai?3 zu;eRKxWI-|Fb3B4^3P7}=e}Qk{*A^{*xr1;Ivg@-_0eJ&XY9i(EJzAHJRjJf2uuF3 z1mrTkhIn`im2plipHBT$rV(I z9^<9^ZOSD%-tSd487@{>OAC~k)8pAWGD1}cl#o6>!txNxRnCD8?DU9_DPcHoWYIW% zY)OOR1(k{|#v9;J3iv$k(VtoTJ3;?eYCbAGNvEu$Y`d?-B3y6+N%-Z=)C=T>K$=Cs zs9AGd2wbJ23j8gj#{IS5p?Fbq?DAXu!i^?|o}D`liG7yb-l1v@)ROzg-4#*)1xrY2 zv&In|bA)_q%|IQGV3{c_`D~>zi5d^+h|CkMb z!YZichs23y#NMdL7Mjlj&AR4ZYDhgui0ANQk{axn%{D9U^6T?9**Ap zK;tfNq=KA}h>w-;2LTD1dk05XXuk$=G#f})J?2pj{s0b6x48Ri-P)6*(6U zUcKnM{oofv}RAx z?=FOliErTC!FwiTF}vH0H_xnh;oq^?NAi>VHb5(hBbcx46WZKo+(DtXlaEaGKGErz zOhRg_rdDHuY|L&-sCbDp7IxsJ9rT|QWul|yQxk`F%FZPG3Ou4U;A2H$#q`q-ELJj` z+eZV>>>XV{3B0wj!jstQnBrGZVLd^X2H!{%ruNQhym0I}ZBaAvQrd49x~N6i5cps+ zR8YozYvLz=+w7=M*-WyX8bh4&c)Nln8Gy{a1;$+#iYggLvUxRd(BMo-CW5zUOIBe)q#f zGrB4GeM?KU!+MM6b;5Jzw56D-(2>G1aG|E8JrPFy{TP@)SS`SPO4dd=^+r--+h57h z3@knSN(AC)^(LAtPCAW%J*_})QO|C%sR`!?0ILVuOL)KW4QHJ)jd43+d-h>)@^(Qs zK;0TiudjA6VIk&Z5xDTm7eI&NVF&cDCG}Q8Ch`I^N zhA@*x2VQZ^JpY6=A95!y+$1W@nupT`owJ4AJgEewvrdi{(A*4*z&7`PQ1{NUy|wGw zXlHHPw(ZW^`qj2IYulK$ZQHhOx3k8q+ga;$t+lcC`QEI(zhoyT=g%j3GDb3zk=)mH zUx?Ya!+b<5u68dK=^R`F?5~7N_Ybsl{8;o(r$cv4OtHEzYl)b-U1YW#M4(UdMk~qF zR0E2@^m|4G3alLlLN(-=Dsw1LjLEtvwLG}w~kOQ9!{J}iB6 zF!yYxk^lL)pw*BMP*#p$KeWiWyUZQIHfR7l`1SoGvpAE&OsNyZOf43Rp#zL^ku=Xs zO|IhN%;D80o+p&s0=rsU#~&1Ih(3aZQ+#GqJQNx&U9$-(|6MiDy9SdnKFMLUd=w3( zz6S_Sj>@BMa(0rQg&GIBA09O~DGwibQR~kqa3j(8f=OfHc?ZM>!HYU&)bto6Tk@(pBEf}!ntVfUipiWSZjQo@ZsnB|zt=@-e_GJz!1 z+zCo@EJ|`T+EA53Wc0rL71NT*KBLKlMX4JGr{kww=-u|>!^k*3Z&M3P@Xyr8Nvq6A zqpVjsH8KR%A>|%bTuo>3p!1y4w!ZRYvELdEau3CF$QCXuu+w4zs;OYMx|RD_(w zT*9KN3uci_^~BEl-B^mb4!o&O)3G|}ajXRTmmDv=N=H&m#9ELFi9uBsuHw~>#lO!a zeh&fi0knzzhE=|S@+#TUp)mGHh*rKy@azf6ak~XqzU=^JKj~OzlRdIxb5HRD1l%^U zlOb!LWXj~)(M)#H`KGoH-8gqmhFsh3oIOyE^F=so0v>pFH`LT&(l?~k&6plSAkP@z zm>wph$J4Fai~BupOSIHe?ldbfB2FxOZ3K4dxGrbz%%aj?!AI@)a2&EEtASFaADx~( zJWW`VrBFQ9bT_(jL_UXdyW8ZK)R?Ao)&zq&5E!Ae*5J6wsW|wZ^C_H{1}RAOHnd3h}m<&b_rWm&52CmH@S8ZT#Xv3;_JZK@Qo>|+{MpKw_? zqIQjRs~q#GTtYFbbSI%NS7I&_DE`S7%yX}U-QnQw@1v8a-{XE! zY(&{Gbx&c`Li>=}LN$Y8>>F{JS#$!()-G-y!0U66K+fhES+Y2}W&Kmk0X*Gy8l_aP zMu>2GpjR{qSp%qLRfgM8e}&l2{9yj}kfVak7PFaE>3fG%kKM&1{ECpIVTqG6iUQ-U z44cnjZBsH|YsH|s3YS24*VPKH^L!h$$WM>7W0vcLuYfH(5>MUTv$ne7=NSrYVywYu zC?ObJ_JB%ZDY1YOL>rNe7lBSnX2ZUQ%+sKPg$t=)*pBT@$gPdsg4MfG)NYrOx<)c24$EHN6>@3Q<7^|Ctk%?}?IL?JR_7!~g zmQZns>7L{tKW}S zVw`^}=<#>h3@~E$JW_c%!LyKRBp(>1y9xUZK_PQU)OCJ3MiIQr$L|ia&BApRhKR;h zKHX6;b9ZXAZXNHS7qatjm=bNgcWX341YQW$o0$I0nXxMzQ1~p5L8Gk5h+!)D_!1g7 zT`j_fh)F9J$VHcL=M_V6mLDB9h5KuU+pq665>AP`PcY z8|Pkh%{BF@SUy3jfBD*QEXjhK`U(FI8f*^XgdL7UElE{1`y#>HU6Q$O!qklF2Z7!UFbN`Q@F0-kmco zsb;%Ohrv3P>7Y`C7uY0;^^7??3+u%9p<>nyCefN1*d}dPyR0NdS<+ZBTKfS}#nUpn{0wbBKGl#?qG_Gr zqFExdOpK;xnUgP}Z~&FY1&C>mIcCB`@7ga43FG~gH5&+ zJU469=J9~(?V$F9h3&%Cpn85wM!tYTtGQT;MAC+|FUq%}$bBkZCL0zv_%l=e!8LW5 z%47!Y7%gYIK+M$D*yKA;mdngUDCYY}o~@3p-7)@2jyD6}9+zL0MdravdfN(o305pa zqugT%!v5LwReRRZ0gCp2LS3eRip2N}UETi+zH|O-^!~FC$&V2dR7(;nh{SyT`fSw~ zhr7Phg|k)EVqiEc4`D^3VQ+ItUDWl6l6p*jm5_(fK& zmsLZH>G9|L9W*0QNjA-2s3w{IDaGPH5&1t%)Bh_$9{=!=156z4oE?lz9C>;Fsr~J5 z2PMY8b-?|b(_D$^KMLXgR_*r15PV%xR{r~yzmX$=uLILpV8_3ky8mmC2Rqx}AM>vS zkxWcne+j++_Q3xZdNXtWSC4m;nvUImGqU$%%?^o5C05)bg#xMsd&h-shxsg{}Va)PbzdlpNgv1IdM21Ic3DAx338Vd_YONYZdYevf62!WmLgaf-C9Wzr` z8miXK%G8$5O-m|yc)jM}Ftw4WIWXF9N>qDHnqP%bHm@FTv^{nhdq*ilv|8tbQ$fCb zlgVdZs@0L&Z+{lH-aVPxGooy_eeZ|77`%uc?!25>(w>AO#}-D_x(UL^w4>_k4R6T- zNn(w2u91rv_K1;V6Xw*PhrtfJU=GJvZI6Jq2Sbg6Y<7V8p+;Ho?y-1E_hvvIsv1v8FIn}k*2QqxR za5)j|&4rbAijhVW^&%U+Qd{F;IlIC@%_CvKZMVBhvKG>VWC`llARW}Pq7o9S>%-k; z7ES)Qxdl8LoYQ7*=mZx7A-CTO78vCs&)nx&M3V7kGBA|qgs-kl6 zo4it^Ahn$kcc77xR=<-$xzn&4wqWwTGrn90>pMDrD|8jrKxg2~Tvxz)DX@wQ1PIzH z`=mE3zSL2UTTy1oL^U);3QF%T4)4VOton(hS#mv6h6Mj$_NF1Ihx51%w7>qS;xLzx z2W`~K1h2FC%hQM^lCEzdRYva$@g6dIc<^nw#+UpLH5teq)x*) zDlx)1N#tgG@lX|g{k-UzgOgUx`?kKpaOQAGZ%{w1fGFt$Yspx>KW)8;kXA);CZyDePk8HnukbTMBr=tcF^S$SO^!s?Z#GYA}ioZ6rm)p*t zRfXsDxs~`2fw>mo4P9P5X(4V)kSd1T1-fNSKE zJJ=bunwK=Z-yIix){aEhp7CTRdv05FJsN5&2S!sSQs{^kl&7dORGxXGbu%{xlmrN5 zg@R(kz|j0!1f1)MyJHx-%4&4Y4EJ-J7NqeJ){81*$jeP8Zq{d`l@*LF`%0>pMfC#| z-{SUawZn}pgPbjcdLI+(!QF_9byCi}o~z}p*2`I=<;^QI^jpmcF&zDZJ<`^W3ezM5 zdkQ@({lW=leZh<7>gt7M}BjdaOd>w6wxSyE-fnK zv{I}m0luXz-vtL&RDyDX+%$Zg;>oKrV&b;absE<)n-zQ>4$v3L!%@}0+-CKMju zrAhTkXa(=WJy^0}*2X6AmgSt%uM%xgtvxk5+Oo70+(klooxKd>w2o)1`2s##dorh| zpaT63Cde_hBcz1Da>wp_+XK(KO;uKCULDUi`*XG@?n-1{xUipq2ezngvEYu@Fmy;mNv1E*iae~iRt^=d8evl_agfbZgjL! z7wea>rv*YtFmuQ~!H)AX9U~62x(?wLOO8vIzG=nHSS{rp15B}5QJ?jnghZLQZD&j% zR+j1NDr~8jm(US;V`NyJR*X(7PulZwEgP<2S1gam-k%8bG4L~_?yGb6JSecMpHTiT z;bAY<#=01x)Z}P8)rW$ti4b#=_?yHC4>VDHf8^hwr(`wH~UXV&GI zg(aOf1qye3^$VbwT|%BZYD*!edUydw)K)GRzXgA!LAJxTY zq+>Od*jLqK^(yEeb5>riW5Dfb+`bdETMU^dnfjZ+~En_`@d7o@noY33KoE|&56rdKo6llE0Xm}{k; zb#o_U(gpnBl-m7{|ItI7HX*zOp=2(7L$JbSmhJL*s>Tz8FC+k5~+Iw8`_4 z%1}DVc1ffN*lsNe&qZoAQ!(D~<_-HWzv7hy>P^uVVv;u0Dj{9lKEL`@Z$#-hs<LJ4UN2d%22sL5y>goXa>a{cE_yQxz-R7UD^ek|pYNCsm>?b<>uUo1f*9ABGuM=cv%2S!==LOw>rhp*=aXBYBPyti}&=Gk- zC{Z>j0kt<#T7^>l9irAmjLrP;sGgqwG#PXl{zD1TNw;X^7#7<-_e?UTG|JMV$)St_ z6uQ@;r6+1QZup$>Nwy(vnlgIq$g|+3O&;efchfb)w8$89Y{9}!o*y~)Vxw91AT}{< zE#LM2ME!gQ)G}IfCf@J!69sPyqJ`2u9>q5GQzBY4<$ir}cJ-qvtFY*mOHV zE~uv2UhuJC$sQ{8Ig#_{<@*4EmW!`gsFKNw^Sf+mFXYP?vD=q|(y~vxU@Gj0L2R<4 z+=tzC{)nT10nG$^&AtQP;84*RPnIR+1dS|r_V)#K#daSP(203RwSNHZT8m0AWhARbNwYqdF91woD`qClhS1VA3%tE;Isj+>*YCsa)G0LOG)8 ztb4PB?QY1_adTy0zV!>st?PtvW{ThErI|H19Y3vnf=)-1)z$`qk#N4_I&H$TonhLz9)DC-yFo5(CjfHG#<@+F+CD zUrkH`dm7EM1;G&Z0$Er@$^8XR`rDNLZ^QO~LXLmr@%{f4a{S$!#lgz)k4>hs@>kf@ z{~%P5`LEZ2|Js))V`6Le)ltpF#ra=y1uNAyqH#E2y&u(16qN?+s~xE#fY%B|0ffEe z_8SmiNaSLwef7SzW7~S6hCx!2C4ED89emWyg)*M|fQXDNqt%T;dSr4@*}cp9Dq3lT_9?ax*&FdMx#wsSS_Xg=jL+Z^vr$)-OlCg;xdFT z`OnJp%iTvU|HT=|>&I)uO3muIHJ4WB3+38TvgEKWN9dYiTcS+e;m}vSpkwo9Emt>I zbEjESse?M@yEK`G0ZemecGVL@+J496ZHCO*maANyfnwVqwd_>R0QACYhecMsSM7AEC zgURc#X6kcqsu&`mtUGHl1Zg}qTXAWX_D7e+d-an$TweQ3lz(axVnZfO6vhz4aSlt8 zV@Uj6N}B51oA8V)kxVtfHCFi0F{~}pU70_oC^nh&Pk@@>GWjCfAQ2e33QZka-u5F1> zHX#h6p)?*wPZ}dQ23-fw#A_-K45jI5qe6L60Gm#aQDh=YCZz{bQ{T3Gk2Fg0ydonl zQS2|23gSyx6t9;Z8+Xoefw}-lX4omqW&H8(&fe159s2u5#kjT9zc+ZrYZs$yDsdOW zW2=|C&kz3%`saM0cQ4%eP18+DEp8rmC#=mv&fFM=*BbO*N(f?GqDn4prKuyC@{RtD z^FT=?;3S?a+^L`!s5QPu03^@#ojo@=`LfTDf`g3eDMiB`FQr}H|5}FeiMt3aU zi}h0Fm)1mh8@g!Tmt1)rtd}-l;P&{Z&}EP3{R3MnBrg~UIpfe!?Gs07XD72zXs*|4 z-!R^1e4mkT2LvX(1B`}OL(h;~)=pQ*XKw)XNzh4e^L_JxZYlgshHU9BIVmtBc>jZk zkR*i^Nc4U-UUNoaqXL1Jm{q?hitn^kBwL(Tc-DZ%V$e8k-HDG$sO^&Tmio_he%3tL zZp4)E+CShB&>`P4dp=jHQJsJ-P+Yef$xF+_ptGoy!aJk39YWd3yM{m(l#=D9jX~JY zJBD7(303^O>o{N<~iG`!b;3?*yaR4I6c$G$m!k5lBey(KWi; z{H>4PPBbnPG9Ke!E4xUe9m7^ZW(Dw9Leaad=28Z8HnDtVR80xdAd<8KrYIqm@V9Z%N($GWXYZZ!f~%fPYBHo+AG*} zAf9f#t^R!LRIWu7dlZE&#f{EvkqNYSiNT4(EFcQbC!uxpE9149l6kxr1oD=;xsxM$~Q&T;6)$Wk;s#mmP!-LGsQJ<>f+f^5KfrfLKpotudvVH&siBey~Ry`o3QlD&DESX#EMv0I_2qp zm9rhoCDKfoS%tnmOgFaTMAa1}idk@_UG-k;2oZ5fLjli}bg{Z3C^1 zqqx6Yh-Y55t4f_v-@~0GL0qvctbr=`fBp1fx&q$jN&Rs!D$d^p(iP{ktZ2%iM6iF|xBECvMnD-Ib1B{txZ0PFiNnSJfpDj{u; zVW`TvvIXe9u0LWFaEg_-`n$nDS6ulN(%~Obm4$$a!FZ4n^QpUjDivu2&09&mrxbUA z!#@qX;N=FM$`>BuJS=}Iu<{Zo!I-VTD}B6|C+eMrT+O<)E6md~uLO^(U=JqO58C$6 z1>YQRbt7@qYO>{!iHNPicwMOH@+mDmB)>-&heO+l6=A{bj2}|MN3UGIgYmP6eG|xfaJ%H9 z{|FtB#VGp~O24ZLN5A`HB#6a&5B)M&Y315}$vBbONdQ*;AOyCDv&L*pgU%&V- zqwBB0oE*g|dxKVBX&$slsFJE5gSHMRV5iEth}q(f)6#r>wf zBEm&Y?fm`q6#?P==wX*hzk&n?lc%~oV`GR|Kc0@A1JK9graGC4MiW;zbT$<);N~-W zHhgE&>+43n1Ho{>^Hly6331h2bxbpztxTH+9bgTf17Ow^czg?6vCG~o!k80NB;XZT z;xY4>qmU2j%pXCkL_UXKzM7--u4Opsg(ui@|M7stKV@g0(Y-F*y*r@kriby_q38#I zRnw}p5i!`Wr@~u^`yVL@>fP;YhbKeTknDf})Sjyu3B%-nH7vyUIsj{i1UH-JE`w zt&)v_g-%{1c4VDz$yu-*G&agfJ8<{&lI;Hp!jJ>RrB23%wOzkqW`|7o+lc2DOpRj4 z{E#K=O%@G8K6IKj)fX+U`lHHvuQ-B@UJoo#!R9&mNh77ea4gH^0HPiI@&q=!S=5Rh z$nKg-6^TWLfO%2ZEGAn@<7CHW|d6OMk*1WecofR~Jd=QYT0*L(9Ab zFFbn*nfQL0g9Is6$c{>u4kLmZ?5&X8c}<%C412yi6h^;@q189>vXnq&C->AIbOljL z;}Qh@x+^($Blx*D(4xkzCUkN)nVRGLgZ7zY6A+6eafNqYA#d(~zJya7)x6y~NDF;B z*p1^HY)7pvhBGI&)P_DUrv1xO&5xcX5ZM<%?W`YyK8g5E^L7dz^LS?$&lr?Gm#lO^ zSn`dfA4OxuKt4K;fI-W<6xks5VGfTx9fmbV%!%ob1+}Cyv2lI^C^Mc=G`?M|yzm9* z#sHh2&B(_Q{dTvUpMvQFuqTE<1Ojn*4Qq3(I+Q3_585A;&R&L3H}2#YaW^Qw5W~^y zsbu1kJCWc(UMN;$Ypql#O;@BCr@@>X6q_Y_C~R#%)96Ld-X@GJ%oCO(pv&qp$2Zg8 zX3VSN*9+pIiyDP75(7K@w=f6lW+Ixpi2xj`6Y3>Sj*vc=Z~Hftt0d`Jkj>=U&@NlC zt`I+3&1WKb#vO2^L1YkI1&*8fAs^CQ+{R?fNc7nZJDQjH76yf@4S^T~i3|+cjHBrA zW)qt;soY(P8Lg)F6_0V8!oLBb4|@`Kz*H;z#IX#x^Gm>fU1-@>G&R7uMrxIkFoF7^ zqOANaP%J64VF~mQxD)LF{Q#_yO5(}h7v6J3y9M=+QFADdaN@EV9BUF(2C;>#O(3sh zTYtjkPFW#zqBOM*OFA%=>I!p9lvH+#!(-0uFHs#wVsXz5r#H!LC zVQJg>_m(=Vl>#6XDE8+;eUolhWqCZ^KSE@)M4)cFbEjzS|SZ$+Sq17jQQ8r1^|# zm)KR?Fpt+S7Ex99yofn~au|Mg)1^9ZciZkeYfpGAM0bo^P#41d5PbP=sirXpv94Yu zT#`vbDkfE!=G+qsySHF4*-%l0UQ(8oqkAG`G9qt8wUU4W0o!jIEF@Ra#+L_jkYevL zO9J1y-C6>2U)5|nNQ}rOGYi*r!C^V)@w4%Z93GSV#=)Bfl7MF6Nkt?&D)cWAZdNK`*=v7ogPk@Vx|z&^mCaBrvRhyIr$1b8A^LRyoYsdQb^fK0PRjh=O27|Iz3Q#+2W3_wzO`_`9SiqjSbU{| zIwB1;aJeZI+TbfF)bLZI9*UEv^t=nm&>&*W>&xdtz6gppTMb z>K3L%ys+=Aq~(m5{5z^Zf)-M%g1Eg^@2B%z)adTG8<>@4JD;3NN|u$y)kf^eRQs92 zT$jF#_TBF!(9A9;qa!{a%V5I!JHncK{;c6(dtsrG0kh@U(4N zH_Rv$;<-g}4qwGnBlw>cqAZ4X-x-5vV5eQV+e0K#vqZ_AAG8UDS)lms??`n)-DQQ& z`p>_e?vn>lAfO~_(^^9`B^+W!J}MpobYeosZ^jbwoFOh&YZQv(=)rwlb65_Mog-fl zIi1$H`b}2b-fr$dI@+=tT(4{e1iW5)i61`R-yz&eL?*qUSfpZPZ?fRiqv$ijs-V;$ zyDNgIf`CVD=nOD6U|@lKMn`3<7Vn*}>?BAL`Oeu%=oT-3TZ&~xY2Ma$0^+~}b+^BK zI@NMPVxs{1%ChqnYq@u%yP2QgT??hJ?StK=1D2AP#}|K%HC=GcEoVoJx`D62il(ne zFJlKA=+SZ&dv8&#Gn`;!zkXVnfitKL(-&CtQMhwUbF^_HyB0aDIyu8Uf@VU z9-ym(75q!3Dn}-%YD+LGF6urXAHV$LM?I4k?v!GHjrm(Z5qtQxfT?52L-wg_*3Z;X zmqXQt)U^wW^>>uG^lqp){+o@sVK%4X(A_Ey3OK{rzEKnAh`zK=u1G=XMx&5Pxdr{< zy|^tL%k8d@YOBpD%mXx4zZxg|v-p*T>C1jlAURS+cG&NcvDQ3=Wf+O%<<2bFl+H6m zdrg=N^kN&8@GYov&1Ea8Kl!vF&Y6~cx1{2wW`dDqJ%n}cjg?H9W+bLbi52Chc_-)g za!4s>iW!jLIrTA(BT$l&L%7~A7%9qN962xn3OIX1@5YH(Lp5v#+nK0a88`$~Ji{i? zrFx`(ybj7B*f8;+5|}Kw=T+AzCu*{2-C&-|Uf7-<#;wr*S*2Jdryi)U4uDB@v*M80Y4 z9*!qJ&`HR2-Zxb_+1ZvraI>|+NE|T7!^-r7=D`Yxcp?KnMuqF6v`l7 z77C{07O8re<9QKp9|C@w2A4E&{U-IIb@bsweJi1Q+LGeTf7Bt_#> z9~44`pg`b8?jG$+FxQAl%>44rkR~}AK5Vu>zKBN62Zp0T9HHynnFx7GI2;d`PH~g{(?0KQRi7`H@F3cr zJ#WOm{h)@%_r}SzSlRgOiR~dwYg-wQ7y(IUctA z$|P9a}(;PD+??;z%vJcv&Oxl_WNn+Mjn2uE|^`wVn`HQzb@Y{hSFepohAckA|F7> zyLPS8(;;%4{>ILvVNGF;e7b(WZ$5h{7)&GcJ0%YW(RIPmr}9v71n$bWPZe5FKyR}1 z@KlahHpg$_idk4R@vK2G&|U5BmK9Z>hi;y+wOz)DD_0JW4O?HOb!%k^;HxaxZtU>a)$WFtA&-9ltU*5iHESY<%u}s9VPr1cU)LM zNI~qc9~L#Cs0%Tv6%36@K`kN!2LqKByR9T#c4SV{%E!ATeiN_!#8cox@GjkQdwIg% zC*;fR2|b4Cs~b+YMRweFB+s_}(<#F7LD;Ue|6IK0=R{>M(AeNLcx;`Y!G}8yQq7}x z8^C|~(@||isxW>ZJQh-hnX)vwxAqwYv2HF65>FGe$9fT)z$J9FxkJ3ZdeY?*b!Q@e zKPpafpq~Y{!w*TVLQfgjp4Obz8T+15g3-6$wNnwv4|V{wg0+)Jpxra!)pK7hF7|F` zuh6o-T_OiKDKl-wGaX8g9tP!XyBXsAL6@4X;d>tpX`=?_4VqYx*we=Et-txx=oB@Z z|ALbEzcQ#Xv;Hf88Z$eKGNVEmI|~~aK(G$z>zx?+AY^~>&cAYy{aex0zdOj7{}&x( z{}(jKmw4^3^lJZ_6Zvn%YyX84`8xa$H%TmirE2^8XZi2>5jKvmI@SMdF6sQ*B$2cr zdFOno+IZCM(~b3z270Hfe{$Bw`O_VXT7jk~n-+dAP%T`pT+z$n7Ol5ypQMd4_Q$Jp zFC(syQ^{)g?y*~rxhqHd<>mO=ZE>_@*@oD#VbO)sv$ego)Dx+pNLe9j`;)p-;-`uR z)h(-99Tm*_pNX5i=X$M(jUKw=$V1&tx#u&VkH+ok4CgMiUp}6XH-W8f8QYsaPh{vz zf`dZ1?>u?h5MiaDUa_mfRa}=pl6p0_> z;zCOk&nQ#w+2_TetB7fu!hAA5-pNBcG4l^7jOxu_t}>L#pn~KuH~{*@s^m>R#I7h7 zyz59K#a(Ist=oBNZ&c~#T))IoNGg^jY82DthWp#iOey+d@sX_fk{418lA2vem0Oq{ z&s(y#(>iO$lygX|#!SK1XcM3x3|~!dspivA)!Tdtu;{LQ@lU6drkwQfo)vfq`d#~C z)}QMX?E7hW{T8gn*h7D9o?gj)|9&mSvTmlL9X3c_PB}{1hV*(kwXbK}{TiO@==rVK z)p=oYhNN&^8S4cjxBRAHf*;URb9Tg5C|5GfXAveWt-G*ZzMHU#-l$b$f*KkLewXS{}>B;b!jG2jR0YE4NDR3~aUX4u%@M11QCy$PF z{iCL&um9=IDrx+lmOmmImsCy;bbNWh({dUrq1AS1Ali{g=r#%$bW3oE5TGhp87LllY)08meMAC_ z&^3h5<+t}7{rIVGb%aFb9EIioJPU(Mx^?XeYfK)Y zp6@*Lqf^(~m7aVrgBCRn4irR(*Y_G^zVhzXOMWLMG{tB(xQtW!$Y`g+2g+UUDZM>NRL` z7@p|>5hhgKl;R*3`Qtk}FXuG{BXT^ccGJ!-jF7%8%~Cwa&Vd6}xrdfLW35-K{KSRO z4y?~cu!}mxTM}E;A%%22$!p^hO@@aen{#A#deFmzF@Y!Kn%)nm9C#eQa-f`frmI0&{_?vsje_aQjWE%t z7?1}2;U@u*1~ZH3OtimZUq;OK7zL2S4%Ea35b=3$jdbfOCk8?#IB;^nr(wd(NC~E~ zBHPi=0VI%Hff2U%>mPlRIQ}u_;(l7t_o#Wc2dUrl?Ph~H<_fFoEf{5fmsi4hng+>U zCzu(Qb-+RQc|=1&Q`2Jzp9&whWIGslt1XKAa6*Xclui=e0Mz3Xl0)afkdw~N*%ZL% z>YrY$hD36C=3T*=_f09<%8kV6mmZjV)vl{xkxN2wW%zZl zJ^4J}RQu(%KYQskDL?!_gRBE8Z*c|tu3&pJ_BjMOcJ%N0>#` zav;yCUF6PR0=}#`%!NMZ+M0ci^pRbyF|toghtC$a&SC$Mp=8;2Kz@vpDf9rnG{EM< z_->K+eu`DCe-M`-H87*m0owrDI0VrM|Jb`#154LVdf31a_qCG7))#OV8n(#IO}2{# z_h=RHH{9$3C2G8~6cex?l<+EfR}YoA&B{zA+Sf4MuG?}XKS49p#w9noG=C?6n+0XHJWx_GHQ&m|9n=8Hf0$l7 z5lt7IgNjKLL*dq4LGkfEVF}XemfomerKPfp&+Cm(y&!0Zl!-JH!-9GwjQ`4D|7bxy zXe<1pwKm|}b#1-^HnU@g$40%Gz?^fr_n7rn-#+kRuRko|YxjEGVnR1}`?>ZM*0Ng8TzXmH-1zQH zS}iq+FGq+I&r%!dY_SwpKePU z4J02}0|#Ca&JgBlEmFLj$O~?0VJFq!$<~DEPwO%F5RWJzizE7bU*W$zdIu<59fwJRbO2nRS#u}>TxC-->b zenh4|TDA0u8m=L!Tbu?oBB?r~@3g>}?G{}zbZSt8Z`FR|U7&V~PB=Q#1asR;u3E`h zM?*30oOcC=TZy?C1(IC30l>l9%=I_1$gqHIPjtn0u;X{$ssJXER+j_t_;UyQIxtt{ zj)z(hv!4PCM_RzH;%3gsl|^4B4=A_-tQj(;&`R9hfPvicM_y#D-JS^+wFd?ZjU2MJ ztsbv_a~C86L^)8(UrzvLy}G5O^zVEEqSZKOimdrF7&P7x-z?-_>UD2NYse66ylp?m z4-^Ru?uSnNYdqga2bZwI6T`qYjQ2b!S8IQ_#!zU}n2P?~k-RiJyFmF~-Ia+EEh&dm z=-*;9SncvBvZ!)IV%*zDBx+#0eM`MIf& zJ*A_r`vQ2wxsCo9s`4E^3(-ltwaNY@MpiBGx=KMc?Y@uGS9H5Ft_RO51A?CXC|~oE zPc~!VM5|AvivQC72bXP!hknJWWX!p7i0~3S0N!}|Q>_d6(LB6Pt;;YH@Z4-83inVhfl8$2_Y}_+czuuGO6Z= zPLBy)b+QT-=;{iwPoH0BBS6t0q+IIg&;yfaK3nfk2^rsCb6(=NP9{W24;@W*Qh(S;N}!; zlqGy~P18n?hKNIVg;-+P|oxC{4xnvr&jwHbWvJ52F@@1ip zTBpdF+Cu_k5^m#092abM*pn5{YnLpS?8PR9c_#;tan+D|K*frPC#Dgo?ZviWJ72@) z!ZUA9vtSPeZ;QxN`F=+oanm2#_C03@_mxfwJ1$K9`($z{XpRAl2Ko;gO296iMPh2- z?Sc43PV(?@tr-2hv*-Ecrs?48RhcBSDy61TKFyiM^biLwCN|Nz&km^@e)Q8wKWTvbBQNFrHkFBNS*G_8;)vVwR?A7kgA44l?vVtSF_wTlNm~9eR z&3N2E`a>8L)A@g9T*(s<;Jz_{3S?q4!cEt1UM<@1ndw@WI_E!uR|4;1A{D0+WWB!+p zwlBO7_&eTb`34Y#HZ?6KVCF(tjSpy z3Ej97#$?X-CnZZPc%zHG6`1if3+bzb%aPGm-tV_`5O+iq_ZF5Bm?eNCyS!d6F7IEx z8*~mQLb|*?-@7et4~uD6_dIm3R$I<8GG6qo-wXt;r_3iE1Gs5I+ zUe0I{j8SV|W2UY1)*qA1aYMCCjghp*l8Lp?j}>5_()w;#B3=ZPK9J1aaLBr9{>qt> zNapk!*ESy`6^Mtylo7rv)bK~XJbVDbSPgKH)F zXj~Xq7$E!n-(<;}YEU0y(lls`}`t zTFH)pb%f|6p1EV0%$^QEymLu=d@;AOAn_(XoDs<_27-3&^Wxc+%OFW!gkTC#rJZPH zqUvr&UKO*k=#YD7WuVbax9D8&Xn-(qMSh47c$&K>I5luDmPV^}~J2Iopnn`MM4ikjiO_a)u-&VB?O2m5EUxb8t zh4O8lO&RZ23$w=xA?7=iB#{ZQm5KWKYQwnDx&4h-_yf5VvA>E4$Mnm1>M_azXPE4* zBF2msa*Fem6pPpTiJzwb7bTgRkWyIkPTX=*l=o))Uk0!+f`^M9Cj?`{99Pib!gd6ng!!ipiV0{CQD1xf zj_rH5b=wXr4O;Hp7QJwLDG6)^{G}`&PvO%yLR_2_7dA}vo1`v-qnc|D38Z3vak4gY znUO`~{c|J^;rt|2leBTcdIfgEbRVTu=p0*t4`^o1)d0{fDf6<*5esGqE-N*lMoyM( ztx{~p;YlDaxTXZ57~&vi5(?h>bM%q$hTFWsQh~Tv`z6Bh<|jzdpY<4!5K|m~Y&lM& z(lGEIyS4~(>b?V03LfEIe{5y($fMSfWnYeAauXp#F$ytY>)VW@u#oTJ(`sAGF|$Q` z!XI>b(iv^NK1pB@2stmpoJ=xxbNgzxEf`1JA7MhvhE)o`%t9Ms;~=##Wfk~xApV3c z6X=Tyd-lA>r^COd*}$g^&-vXht9VTlBqv3P0`=>0YXjF8M_qaVW|>gc^0lUGz$^`4 z^#;sA0y^)@@7WaP51dIsN_yLNBBt|41dSpy{a@vn)QG)t)F+j>w__f336tER5qjoj*Lrxpbh~_ZcF0K@K!)Fgr`_ zdYn~o74YmTt!bjs(8x1*kgcH;Fshj$>ekrpGK;AG9TF1cYK7F#yux)D5Ph_9R zb9l6Mr+o`g?fsGPCzO!TB%NFPNB~19oJ``@VUW-`44Z7=d+|W>aE5kV4@$Me?#kq% z8L|rpDBjcur5XIE`U#pI4hPR?4lO@`Yh3LNek5H4gu#8>?%eYt`(;Ggs@S|_D&jA1 z*5jd(l)=C=?&R6jKE$T=2Y5g5w=xH}P$0wD?&269SW#kS7ftk#x5i#4v1P{Cz**Jv zFpA{wq8n(p<1XO@bT!sz3DK;q-NnkVNrI(M+&rBTb{PY;K1OOj5UDS?#d6Vkv0_Y@(O+yP zM}?CCD*vPE;31mIgQ>)u>T|c!V8@ z7_IeN$R30Uc+zsx`@8g3ubwSJh{ujvvFX(EuwQt4S|)n)fKZ#(Whno05~GzUA(2{D z(2R}1IoUTD958b+Ur3ekx|lSuxw{AXwf@TLs+C9T=N^X!bf)tQTzq&5TSS=7O%Td$ z-ZDB(usBvl6^Qz43*x(K6^tiV?(W*cXI<=gis_q&zFl`zJ)oQEtGlowaET| zsC&ogO5ZlyH@01|ZQHhO+qNr7C8-z{uc%_%wr$(2I43W<_uISwy}QR5=fgf<*Oz;& zXNCs$yx=DCVOUR_HRS?{?Og}p*yPeF?^JlYGv-T( zkSxnVQPrQBX1vre1j23n4ZC^59A*N?upD@0Jzr-DYTXwG?8dZI%(TR*4ZsZS%m|u~ z+01C86}?B`0lGa3w?iiT1ndU65z z7565^sS7Nn$Mwz&OIR$rYMsB~af$W}zV)vG$uL*ksURKhIZt;Z^*GF^MyWznrWAoE z%dvrU*!e=(-EqWFj>-@hEce21AFLu>6lr;2<#1Yn+=C8`*|fuKfor2}Me zy!jz(RAM!l&kOu#JX#L8u{JT!Mv`y0OtW^aK67BI))GZfWS`tPy3Q@UGPOl&+}k_KUV7EPJE%{IWVlRz>7o zP8u}O#=dA%bpN?hxf?GScBzy|(n5VwgK(WCqMz9@DW`9q$SEjP#e0_7vS_@7f!9yW zE=bLp-Jtq@_f~-WGqkz{?QG-HhgpFN@8#Tg=@*~oc@yo|IXS&{*ZISvk~5u$CU<$< zGWx!xZxy1F=%;pANg_aRR|VaAi>Ux8;t`vJ>R#9U;tf_vAK@~d&w@OZI#v}AT#0bw0x ze3+la-@U%gcspr2+~?Wa@ehikiwGS8mgwa=||m&7zq`qO75fmBS!{?DO@RreZGd16z~N-oUn$|lN}m~4IW@y@Wg%d^Qi;M+Ji-zRi!tmnrbi^l zl}fH+ecFWaG7m%f9;Yr=YriR0#twYbS(iql<0TGPlIJ>C76PRW=Q=W15s$9^Es~4$ z0uFhZ;dRDfwHjsQlI<#|nG#hq30CGe1%!{#qx@_I%k z>%5je!+9GHbj_iU@7JX#PN)!oKOZo$qSlJnkU=I3>ZeW%MW;pzw5Bu#ba`!&>hFKD zI6xasKw3yK8i)4Mk2vmJi(p+K&8<}aI18$KKg@9h&vi2fKaqwNUAF9@blC}#!)xRS z;E*ir&w4EDx!C3vTHX?N&C>%8-R67GirOh0i9=ESx z?DE!G_ILR*V&X3PC@bgeFySH;QQWGnhMC=TxmwxM6M=Q#L%o90?zqqdz@5iS3Y(+dMHdAFJHihYNW101hwm9fC32ElBaJ08P-d+n;ZQM=8a=h%VRWg!*A4!Ah`v3( zTqBEdwnX>Ah|liZ*p?kZsT_M_`46MRd^mx%*Jv?M!*w#U#6A#v?s( ztW5L$l(jd7JRbf|7y?Mry?z+)WX5u(I(%>prdh9vO8I`74e&&hzWyp^8b4y_6`#SO z6LD>VPeEemW#cu%Op&R^N8wV#8-k5!6xt(Idbo+a4ob6&0~pc{b-YKmN8@l~8lYA9 zUxVQi|(@OCEnQj{j{w{;y}s|1H^uh2_8T{nh`j1JnWiT?a@(T3rVc9Rm5{APff~nEvAM zf8injT?_cf!2Z9qfWNU4UtGlh7h1qSK-B*c2k~Dl0Q@Ho;=lC#|MU3&f1LPP{t_5v z{ufZ>FLHF`vi^rYzb|0a9*65Iyyh#AtE2ii`O1qG_PM}%SqsJFSTteAM2{dZ+Tx#oexv?_1uAL zjkZElnKzyP`@`-9j*~kR7f#nR%8!+hh%nEzimAawDcbt9_3Q!aSQe)ANaU}^oovyP z(P;8Pd&NOw1!txfp0)&od{uQN;wuso0)TQQKzH#>5{Ifjs(haK#pHHP3(kj0cEsk( z=D)qF`?C3?1sFcS!E_M02uFjKGb%Vd#x0%t(%%_^~oOH{zb#s1_yW z)ja-M$z}?X>DNZQaJADctiZ!sh?&5>vunFbZZ98EQ;2WauM74~kpd5>`DAX1a3u%t z)a{IoPD#yy^f?+eZ*9XHP;A2@b|;Em9r&BiZ{Qi2yAtgi+uX8n^_UPiOEY1at>#38 zlJOfrSiCR3u3^y>z~l2LQrW_nRF2{FqOlXlv{WuRFMvpm?c=yMqvYz$`&G9{o_tWR zBXj_i8wsuqTcHq{+ z)EK6Sj4+xqfU=-Q*FHXeceOw}jgx-zL<5zP@sk;wtk$3So)b;0#9DfLy@J;xkzagu zlnv2>RR&rb@flD2S;BV^SRIS~)v7ywdb_iST_9WzpDB%Il0k%Cx=l;$$CuGBNt2ZW zX0YW)uCQ~)D&AUSa_k(INl%$aqX7Dv#xmM(aImIi%dkI-z2n&93pVG|?(Q)A40L*tKAu4yK|z6Ih%G0o?89T*gm_k>Dvc zZHIU7^U6LZRss@ePgfEUQ}v>lDn~}+5GCrwo|XFAkf(E;pK2)wJFz)b-3xTzU-2Pm z;Fj*42e0rU9#zcl@F(!4Uue%V)txij71B`YaV(BwS=w3L;6!VQ@OKVFvn>q6@iMdy zpTfy)JuNe$Dd${c%e8sxEoLEPAOox)DUK@5O?MRrmfIw_fBY0YN?m&USk=@Z;9jV^ zu@PVBZ+p=7g%5IhqzaNlt)iYa#eo8kQYp;|jp7uACM$@RaiFqc=(dUrXO-Nj5!j1H zmpq849z6vv0Ak1M=gqrESh()Z(CUTrku2s)+Ls&eAzh%9JybPHpm>`65`GW zi#^;+y<4ejZ-x9@_U_$LfvHeNK>Qv8bhlErml@x_v#yBnvh~4MZA*+h)mUU^xV?#2 z0i)luozG!iA)EcNk#8-JR7yqQMp|EH)j4u+_wx|_J^KCg@)G*W{$Q88_vsiJdkpL} zAKF%d%+a`w^F+s?vv4Se0*{k?Xvb100k5FM{};HTu4AFdyfp$<%a88sc-|U)ubiE$ z`!<3%DTAUNIfj&(SJ2Gd@v5v_xXN&w|hUVikfZ;B~W9B+_}R;uY7 ziaCU4sWh39c~rQ<1MXVDdLn6$dm9i|!yqBjE`;5!_cm4g<>8uD{h;smBZ>R5R0u&$ zckiol_h=T!(ChAtg#!(l+)ToNeEiLz`cSr=kFD)GHWI4|v}+`#0`SCsp3s8#Je{Mo zEbc$b=F^X1qkm+KwzizxGVxuarE9~}9Nn0XRy}1P4QBR!G-q=o2I9Mue2@eUjt;d4d7O3wQa5F%WON= zQLnD7v?-~h|I(+yDTOxH#l}0hS!Ea8!D!3($i!>i z*L~F2XgZ{R;3Bu$2z?Lwb>xmjEEs9%DqV=H1Bbm_2cjM!W4wB|*9{A;>lsGAn2}hM zjfjJ&IYn>1m(XZw&%M8?IGffA%!g1u`x)FlBAD}{dPz5z+qLmxDP#ZO;BdXLi3W|r zCiY?6NAde(yPWe;L@=)DR$lJG`gdlqEl@q0-Jh>KtMAYXu09A#pM?2ECq&i~$S5@15?otNVz1Y<~_bm!=~7)~ZjFsHZAWDRwB zj(LK7GU|K4d{wmK7*v8S;}B;o?U{E0V|bzHj-m9sOX{HwnR-A_XCK`tB`6qd>-@

cmO*g!@bs=5M?d#?$Rx>A17eIwV9@%=@tkmbSg0HqywR#v40(m=L5`jBlI# z*|}m^^=)e^u6qnbCPo*x+@3VzIaS^FzHQj)Ivd-p$B2lNw{Pm(HV?lmNfa2rQ?z+} z8AMZd1t_X3hvoyPt_({!!z0!q?oIG~NUck>mhQ zvmE>=9JGf$a~Pb1H4(-RyzbG_t;wD7QR<;)54Zv zPN}*KBC0Z%p4;dLqU4kyVg~Jn-8av`SHzK+UUwEK@NO&#t8qqoxi5SzwM&BBeP{U{ zX)8Ed8}enlzQV&3Q?VCA_{;^a%8vW^o>kw(7W&e6m!2-s`ZF_%sSS(3_&4f| zA1p{PR?Y@XHL913Vh+LFA;zljXq`tURC1Yulfed$#M!JH(BuqB@r9nibqAL-j)L{T zXg~f@=%ACb#gLE=#B?JiFH%?mZEy|HiU@Hbi`>!BpZS|Hb~=PCMN zONFxnA_r9FVl-_g^?&>CoEJvsU`iSeqaK3128sg&cd*X_ zr^rk)E4UlyPI5CYh7w7D{tyEOq;ko<0QEFIp(@3w*;W;&0({&p9edfOgEilZKE+HA ze@r&qxQ__8&mfemS9_~G2Y;KN^c_=6yGo(5mIS0>EhXvgU4m9U2(#?0b> zgWgKaAI9eh6Bt$5lWbQ95D6-Lx)doTo^x_0p7;e{7^yHF?*GlM;O0*B;0VjlnCGtc zqP)yxB@RHyC{s)(z#g+73vSSrYF3i(w^MGAcW4siU?KCt&@7Sl!$>8gv|h@T@7zL< zVXSD)ALD0b@x?>k$k>aPQ*L4*?1RqAzAFt5uQ|8$`{#uTYN9gFvn_$l6SqL=DTFPorf4FI;2$A)({2fxClKtS@5COPW!v_#{Q8iWS%tc2;m80 zdCZB>k(kh9Y9c~8+zaM85cNwPNHEyIV#=P+cwZ?3D&K6!x`(frTXSFBRiCXnggMqv z)#w4LyfXSfzacv@tH{b1)w^2t!7 zvp8SZ_dYz1`{w24UmdijK(~(ry*&c3LJm#_21wf}SsR@d zN|C(vUuJeu`UQ0h0}!#*Eh!@)u}xj(@@VaW+^yl3_@tpt5PN8Vziwf4U3(pWvO*u& zYRxkbU-4FZuxTAC=@B$J;P|BUX?skuXsJuCp8Q1`MQ(5Xn+)vxl7?%O=R_xeT^5(* z3H7uzT0?hfJ}+u30OMYi7Hf(f!{&6eubkG$v&&_?jk?tuY~+|w3{t&Cy2bO?Sch5r ztGSRn?zuAQljx}>@%wE&Xo^_edl_4G<_Hx4lOXv7t)X|JrW=@j;4OEbimDMn`r*erPU)6`p)OL9(@KBF4nyqaE=I!t>W3&@*cW-m zyI@|LWJPnnNs8^wJhG;eP*ON4`B|U#zMSt9bs~xQT6^BhpJ0A(-J^f1;xSU)csa{8 zw%9h-;1UXE^G5Si(_3VqR>vf2G}UGw#>gqIq9)k~h^_Rr#wV^lDmEtH0EOsZUzK|{Plmcil)SR%&)w}YZqS^M zIIHOfLw8h5gSV1N-m?V&1xYd1Sast8bKyqw5(KYV3Sr;uyrgxFU)0UezV)hCLX!$* zI|0Hvd+=`B*VF@B3H61ipT8QP`ZM0V&IfKGG4aW}BuSk_+HAQo7$W-pYkmdm8WNRt z#4Xs$-bYVzF6F}%9P~JY_Sbc)-+H z?GIHm!-G|)1@!mFW(Ob)I?c3M#rR6x)s7qt@XlP7QtV^xs8mU7Z+;BYv$@iSNbc3* zcX=$0ajqC-DID|;J|ln*#ihD%atUhgV|r&9huM@fg!&wblVgkUJT@@v$Ox5*v@Us? zUzwu>YhXgR#={5Aj1c%BI2DMsQ2Xt}#=eC=X`p`A9@--LSQ9(!6w~XYw6gk_qeCL> zXp&gpX9yr5HSz|M&ob^D8hAwzOa8nX0$;Hiq!#e(AG%=Uf960lPuv3enO2v-grE4P z385NWgwRmC%YK8LaS`VMQD*05>q{%s`KY?+)Wod&M=Om{1E6X-+xbYAf>*wtd_kS~ zwA!d9TPPpw;EFdDpyZ=`$-Di~^Cu3rbKRdL$#e^kS0t1T-7zIo1H+$rsGt^*Z z>-60H!gb;M+V&n{I$1Tks1>Ht7An+G!W=2;q2Q;Vh+XeA-9WWzBKChFVErwmZ7dgLN`pev2_9 z{$T`g4Uw%Sm0o@`tKIGbPpJxt=z{n~5FG6g5{lU-Xf>Gx1*bv#d1HfOGvI%a#W6zpYRz3I zu10GEzaJ_U`2=Bb2HRM|4hiTTh^%!s3vyKism9=CKh2BZNs?rn9xJ zk5dTJYewuxDPyEd7EpQ8%$&RNdA-f;AHq(Xa|_Ov0cd}u;w!|UV}JD2bPIh0<$y^y zvrF-(j=Ht@a*rF@Y*IrYwpF$Fh|by;Qx&O20y6b`XFj3@XijM^Hf{Es6O;==vg>=v zj|cm?*DZuBJ!>Gnr9Y4@p&fA|!jZ5ba%DbLi7i$8OM?Y6gXz*AJVQ`+RXX=rsahN6Z|PaB{+7bJ z;UsixUBMhlP0- zN!S9rw{m&gA@)k&a8w3KJ6nJkl6r?TV*{kVEg_)}0RHsZJj5}sgq}JJ$NUE_|L zF)IXo6+vz$;R-nV7z;fIZOhhZ0{F10DR@H8<4ywWXiTuBZsY32{waFwhU8=5sJ-Aj z;~;VHG3%sjrvaFJmXqVSuxuGi%_Mr+tPOBz`{tXypd;|;~O++VP8+(QOt5PhDt9TF}4z~*OsO@yt8 zlY0#0iSEWLnqIea!AKMkaXTUWH*Ts)-zq~1&PVXhi836nrIsJfAs)UOJi%v!Zk5=q z`x@DA^d|MeWcLg6a8--`4_vq?bh>;h=7jo}-5D%Hs@J_1_tZ5RP+Z*MTdGDB}P@QzKKHn*igE+0~YGLg!HY~C5=fE{`fR_-eVh6ipwp}X+ zOOI(=EliFPqDJ6s4(XBv-9Z(uUNxdaK}!@b66Vn-hY_Ff1tDY0Q+2M#Nx((>Q-PZD zW}sK)fJ&i;b(dmt9D%Iic1f>QCcrm{K#{7A`}#iPz*M`*(5NmHykpUdh``rjL^lbk zuvK@BbT6hv_`m7n{j34GR6gN0`>qx5rEZE&W4?d6mRKn$6_7h^g{9&F#BqG(Q7-P| z8s~viI7dVAyL*o2MNE_fOjGDsS?g&&Uw+Zk-KiJs_Y)(;fF^@Svg02hVXXKUNPZ>f z4@|q3n`PilApNlEFr$HIl7%5~)b= zgeQyzBU5vt?CsomM4=PJ^C_ZGLy|LCry3yBGiuQZaf37}Gi%j8?QZg%mCn8m#%UiX zHu;r?eyft{G7QZrP!p(tUA%Mjo*9>Z>z%>{HuBnV@BPz*!D&Wz?co08hzwRxS)KK zrTD>RrdY1h;ETl|XUaYA5=iFD=a%}FAz)c>bjYvpYHMVVBtA(CQ^zkC38SJ?CX%~I z57Y;1c(TJKkgwfWan2&a5#2B~0U&EXbFGYW9vq(on@4Ryx8Sc`4!Bg+;D-k=Wr?c4 zy^~^ItO@~)uui#ZM;8!FFAo{n4v32*k(P*|QnJaE~Oc^il<1gR)z%{2X!w@+F_fsFN%9D$#NgF~Es{`88DRvw|(_w_x z&fN;qI)KeTcK#$i^DU$mc`l&!cCU{BXuH+xNJc%E#;_89lNx(a^F`)WFuH^6GZf7% zVVb}<6IUG?9t3RT?6)cm2Y!1>u{uy*2Y>ZZ(--jN_AYy!ML(f0&3FoGmUIrYl!%Jp zs!Pf*mn?L!GiRgTFc(_fnCf+dAcOH&oep=+s(cA#!zyxV0f_;TxqER#eH;w#vL^u|EHw9bns*9nx*2yBrBkAJ_xj?f_O3V zX9TstakkaAcCSRSu_2M}X~SvgHMn6SwC=q=_Yer;gW}XOe~Lx77s?7h@`4ueDRNBS z;A-fwcHRaTH4b1*$Rcrwekbv9iP#P>jcL4?8beJ?CMA_<{b(_MENWg=1TP7Nb&;cx zNN~_p!TPtIru_q;uk>gaf36!&JkVm-V$tb7|!7!ZX=`|6?MXrvpJRxKMF<9}Gr zuK%)X*ChXD)!v)9AUC}Bo)JPB@@UCo*mS2^K8mqmO{18(NML|RM(w@7RFs-0PAm>& zn{o(RM%g|VPL`+?(^oaxb!)THhCW!ITIS+?fnRv3m#g*~eddh;8*pLrOiG8W~99FZ1=h3$(g)81zkioO`r zmiVtLK=6{%SA9VNtA(^^I1R=-|NBr_|1+ulK)@V#Ownhjq$uuQ!Q242#giaRR@$YI zIRPm%7E$4)N0AfvRsOfG81kspHA}$r8|uTkP%jVVQTcry{WHzD0!Od zgNr%mCfwQNlOAZEckEZZ?*UR>(%<8K4-n&}x%xOfG~(6YE>jIJWYbii$AH5}jU$%1 zaFYj^Fqw-uTtjlx(Tro8SE>Z?zT$n83m7M%((9J{iKmDt)6LgTjh~x-C^QLXjiIu3 z*snHv!K7KEP2Q0DV^$}rh3+2X&UzZz7qS<0w78ZjL9x7euiw%L z7qM85?usDMf|cP&Ky8%ZaUvd)pKBRY1V+KJr9;EPR*3P*8N>7no z!Oqj0YlvyhiK~b2xX-0rw*{YM-&0wa1c!qZ7q|$9M-QC06d5A^RkV2{SQv5*-)xD= zN2Y{)?9$o?R2V7Aw}GV~(9XJZ|JiPj!SIeEg*YAa(RabBDJMiYC#qB9iKs^OX{dnF z=8QwJllK7BII9Pie3`a|`UwL_yIJFj0%&w~{3>SA$POLi>nCyQCdD}9WO(x;I}L;2 zl6js(RqurfvZHf+$W-hcKz>ChUR}gdrW+qfnzMNcB+rxS@JBiiudVV51~jcS)x*K( zCrpffq>y`-TbW}EBChgP2q7-J6g-ivV?2Sw=qP@@7W@ytyFrM`hPd>^ z-ZTb}@Xp2#K!AN9EIlVr;ybgWT2&$@SZ3xaD$5dVBw&28h-rbb^t||rJlo0PK}`D= z8e!LPTi49rKYe&@;6IlTZG!?*k;(04^VcJnI3sonLk)O5Unul#)wa*kG#tG))(WW1 z`DaZ<9FY~l_F*FO(ArP?xDjB0RPR;&zi;=&BA`*nP6#>1WDIF~Ryx8qA=5%|QgP14 zmR?sDmPQQ#$KjXEdQu>@IdH~K5!oIFN+W&K{8nG0SY>Y(WR}B_ip-1>(O!7gTBy8k zOTB*5*l*F?g8cz17)L-w;IQ(f!#|LK@2mLefkZ@g=Y|NLr}X%4|Ri+8O~t;+;^;3Md^8kengo2)`wWSETxwpvb z>Zu%o@7^pm6=pLsTg6N~gcmG15+b{0@>BeI3i;KWCbXEat<2u-A5n4K*T2}{&q(Ok z%`+*oZx|Xo5eWC17S9GS&WD`dRpoJ#*v?Lns7|4y`(rHt&Izq>kI3`(@%_L!d7#qg z;+1iO(FpaVd+E9Ga^XJ_X0bbz5#!&4y?Y z(%B-%3TzVDenhrFXkT)$AGw3j;NdNAE!v73l`TAmeMDoemfN!cxsFt5@BnST@10ZJ znn=6XjkLXm!}ta1SeSG`0cD^pF$K@_*0OOV0%pD3%`FurXs*1McUsHF!;;E@PQR9c zl{+Q&Lhw#7e2BMRpp`IA2blmc8nuR_9#Nq_$i1?XDT$14=qZauage{NR;IuZn1sL* zpw?s~&p6~=e^iHc&rDz%+jY0L;bj6FwS$l#H1;mu+)}Hf5u>Xyyyj!7*fv45tQ z$Jvjl#k=;ok62`okavB$}_94;J&Kle5&gr+?|2;rsH!9Uuf8^q;vd)UaM7UG?*+=u4dI#(rR>dC5o?PP2nks2I6T$W$8FszwR#9O=ZsmzEf*i8hNgLIS?ow zXhz}|hhi*Zfy1E4pEjJx-NIxQGK%J1TGnJ?=w6fd=MeIeyjZ1k4qjkn@=N}XiWFv- z(t39z-l73d_#gX5CP}J+=la-=8QC zlOe8cOVc#uz3M+T`=gmveA*oikCv8{cUSc(Mt)eM@k8WmrmMAJF_gQA0f^QK2Kdo; z82V1w4|pD?MzL<7rl5Ga;veU=iR+yH7TCPIe)$%PcVZ zw|F(HET%Xqh^E*`PCBcKhJN#SGnO6+SJzfzVc@aAVm3>(7lygJEO!eroZ z-#Z_n9K54jq6~KAZq+vfGZqCFhbI-1{GV3S$FwX|G$=-xZy7W^7lg~^4RG73xLk}w z?J%JB3g|A$B&Udr+e3RbZU6Qzx7Y`TR3<8WY8F)NkexS zkj1{ii&LU0)T1fvQ~i|en7dezil}ECT0eB!|BgH4MOFn#At7G*z#UONs$^Am9F-1f z(3Lx4bl@xBKyPc}9G@`<)NJUKoc&Ya_rlsP0jBG#pZ1xzRyqM__w~ucHz3o5EFeSN zJ|rNhBMmsBZ!gsMU~woz$y0Fi+3=-mVc(F*sk;o2;e@Tf%eW{owCm9X{NS)u!dY8Q z)*~un?JAaa>-<3t$X?=*=zgndnWTW_!XspbHO@_Haq{ib4i$fo}iYq-!s z;ZReM*pKJ6gSxzU-&OukCH9D>Ikc=&6Ri7a)+s?6L&a$zJK0=BrnVyL`)or2&t8|p zA1feVnbvGtagrHQDAyeRpBd78ggg9qGvQON9sUH=c@@&BM|lbQXRvdb)6ZH4p2*aQ zb&5paGvu4Q41!@?1eAch=$FMmWZqq%l8Gl@F|3{29iI8>*IZ_(??&30Xg<{*xPf6z^Pw`FVOTm zEl*hLv`%3ek^V=W*Jir|_Nq#E^i5hijHi|Py0Lxp<7*75=HQh=Cgu`}FZlGg8KRW1 zpuXX!Aea(pP$s7A&+T95ao64-bT381r+2_pYn-P+AMPO_BSr*|Zgbfz$IVi^rD=-e z{^)%@$x`&P8w+4rD2x17Ghk(mHhm>))dqftv^82%ACe0M!3EwEY6fkdd1^zHaxoS{ zYh#09N(OKJSXW@t$$DT{kPHfZuanCMgJvZ!SlTeCm}aL}M=6y{dJPCQU%=#y*qcJO zua&9j#UEaI9z@K~>)0ys&B?y^J7-|;9cTq%ru!Xxi%=2;AjBd2UcXGEtahu4I_!eGY*}gE~Hl=@@{aUD@&d@ z;HPG|HuX%DEh2GV*66bR21TXS1htpO1bPz@!=2D~+h?cH=&ae5oQt^6JsXH&<@ zc+YOhK6pX&IKh=sG&ZaCBvF(d=mtOCbUo1m}i@sb{;M~;nu z=<Mf?*m9JPGk1gV=A&)}$z>sWscA9?ZP9-V{#xS3N zGmlo1lE^TP_$o8GQ4T~zV+m1_SmwDvUV8YVL_zT&Hr1ge4Ys5!4$DA1X6r~@xd?^# z{3<;JQt4Y%1QYbP^XeQK0~*U9StX&T8OkbKq64E3TQ9cFj7Sgf00?Fz)paDa-io*A ztc2OCExG?l7-d+=kUB6-W7icRn)rhnur&srZ}@p*6EZ*O(Fbc+xa<;h?p_rw*ZojY zZK4>>^3KM7ufZBv?gSpi2_sflQC!KbJXDYsx=u*YaMhge&p&dCXU?vsu%oQX(YN_) zgq5YBkxq%_{D~O6J()A+9a)h8WUw}|xqd%Cu8UqP4&(e1)3ydfk*s2a!agq1A;ZD} z3Kj#Y&jZybajxf@4f%1nf$){({)gW~mS{L!4^kvMs5RpY(LYg`4)|1{SY8+>svldQF7->6$2+X*!NC<95KIq1U%Uh*VpUGx<#juF)IJ3?OBj4^&A=5oHP{Hll%+#&3J?x^@P7NlQTW0= zrtFn@<|=M4y>;YF%Hi?#+cP*JJC}w^?(5Mm#WT55qfKHrtbj_LW`B-azOV1An9k){ zrM018xWLpay~er(jomUt&%i9NJ1ak2>u_U7zKo}19l3#xw+w$Gvn*jk+N~l_&|G6S z8*OAy%vZ>xlQX*?sK;l#N!NZYNp$6M43f!RAo>N?uC>&jhxmak#Y9~)F#T#JEM(M{ zb#^>}R@8@(TtaWy+Ke%mgb)Bdo9O)3&G_Lg0IP+1#&0SCnPXZ4i4Q@Zf29b0y>NpE zwJB^Cjzk_et*ymFm^2qB7>l6q6Pkd2Ue9qn>ndSR{ir1AUI(?N)C4x)(vCu1^E&Cd zk+U2gQ31+b^saR;Vs5y@2oHM=I#=WK(~>NLbZc0cG(4A!ThBO^KX;3oKEW1WzVl&} z+``JlS(}o%NS^XlmU*7pM#Aqx#1*ooNj5@WC&JH;;X* zL?fW*I`V^xg?1Kd9t4}Wu!F|Fn(8BiC(Z-ePaPE6~Yxe?%U|B0u{0f#K)&G5l&VEm?5x9x!4 zs3GGSwq(*_5C?f!wfnNHtjSlK&Dx)wHRSA>Uwg>QhVde8&6*T&J0+gXzX`FPRu-_8 zwKGV1CxlbKmY2+pgCp>Hbj^0Z?*i{WxXK)}Md^_X^m8+?x8Y5g6bucyO`HI_t1?^%qxm^6-} z7V!Eg_U={VdKq_g8uuPMvpxycHU-x_GjFU&h5yLIPK1jm(4dO6gtrE+U2Eh?O_5JB zG}XQQ>N>b=+c!DwEk~$X@mB*C z6)PC0ls-j-DNQ(c$xO|157g+mP$FYZ^_X#*fb(bXj@4_!6Ic>B+}D#~dN?O@Jv1jP zsPp`QNFSJ-m$r?J+h>YU~7X$6ExSw@X7aY3fU{R0lD)bs(HM9sx(2Sx+Gu z!Mj!L@Slf|q8eoSD*nX$aWFMw`@0|htGkfIU^Ycqf^P-%XzlQ0hwaRyllZm>!M)tN!D99o+B#3!{A&e_`Xm%HgYOyWoAuG*(vJOOvK49$F<;>}BP z8g+X(!wiuce27!i0%kv8w^_|}Im6N$quR`ct;$tZl<6}F{1pIggx!d9%N-QxwpFL~qx(!Np{b=~1R# zV7tQOu9QFS%T*5|!xeFbU!ep_$d~c~cI-!ZXMc^Q(t&@U5RfyRm<3z(^{rEEJIS^h~z{of(H|IbON ze^hsW$?L5D(iHRW;iq4pvlFs&{=Mzy|4c}6{?+ow-yh0udQIJb% z!;HQ6stb?NB&-=Vae42ev>1N3#v z)rk^q-P)kUum)|_s>$@N8okZahmEnYR{s%+ve361{>i}D4ny3Dh$aB`Z3_b0;Z%@% zvH*%TmG;T!AxA9YMi_43Q<)k;{nG#oSq#fnjh?d)k~0I~ z$B^nP5+x-HGegRf&J|FM{F>D-L4;n(lH`mc0dHbS^uYT2+?d>bQ&apaVFGl;6QMr5 zz923o4OSMqkn+$YDEdot*GLUYcsuoG_8@kp{XPdQJKGz2?fJVPj2hiY)Zw_7asUMn z7L|m3<47baRs07SXMK`zn3lj;>H^3xI011BAW4iH#R_Ic8f{JE$J!Q#K_{pky?org z$z0<^Uuf|v6!@p1R$Okh5cXFjib%@G3hRXptzJda##vej_I8r=4slzDdqx~{pD(UM z@2o$0PV9^F$`K3~eV<|h1&dXv6k|+eO>+QD$}bL{T#I1?5Ok1r>{26WIM8X4POJF3 za|Sa~6{9r&Km+K7puMWN)d`lQI6ATkF;Fyx^HhTKT!*0f_M$NmacBsz-9K1K{>yCi zhCfm_d{JyrP6s;smR-*%yn25qBY_IZDzoh4`O*@H@g$AN#?WspsN$#^GaiwP0!b1X zx=>AB1>Ppl1@D!rc8d$NMOy;&8PA;^ZBG}71~9+YKb~rB3{8-`8dzxCs9L`jK+RQ( zlNFN77SEbGU1Bwf<|T*L${zgI{EZzaXF!(H`$u7mUj$AG zoh1XcCU~x8@qLsXG%vyl=G$45D%0PLp16J1%CPOim63*4PpXDSO{lea3P>>UjBU|x zm?C^e#EwU4(P$+I6-Qnu+$iy}TM5zjc_LW0%$2RWlsP|I;6f?c(BdnR)}UO6F8ssT zI`YUuHf18+a-YcRWm=ny?A^P6ZO-Hx!wz`D{@&X<1;V7wd7Q-g?F)VJ05q@$!5m~N z`i7j-z^WYFdwQJQFC6!V-`V#`xXD!!$NydRTSh64{wyHdk*T|7(am$HIY=XoM&;sY z&bqUffhH=e zl?)8X7w%?DledDSyGw{U09O?q%(k$xlnjiM8-m};lm6-&I>0se>rPtpFrjy=cG++_ ziAf6b&l*T!j*t}bOcI3a-^vU0<`)L(Z#RA_uy+bW=m!Bs@;hc)BQmXidjr;ke4+G_-1>!;=a@TIUxFhos#lBm!Kc_R?=+MVR^-6^hwj=d$EOK%`2u|8Ic%CmKce1H|rTgpt7CB6=S+h|m5QP};~UK~U!8~O>8zh0>q z@9`r`TBxZCbfQFg8H|X7P_*@WVFjD2w+#)v!CV_pzr^e|MSJ&mF++mt))W2RrIeM< z*pVubRl`tsI=o$5+cvzh>a)|vRSmX+1(pfVBCW!@KL7LERvcsx5_yuJ(70}Ve3G_G z&!$H@^W>`ng4e9*6u+a(U7+S-UghdK#tsx@2S^a^6r>v=@)1Ow6Z){%>U%F~^W=se zcUMa~hJ~xl#`cIWszXFhc^UE%j_B^2k{7VOH^`pdiOFj6HkrL&*9u&@cX{JCkD?2V@r;Qrum1a z-1Xa1nFn&I#H|f-Du*!sKT3dnAg~Y}WE{yzSCH6@qBk z;y1XmmNoD5fbAqexHAa6io3EndmZtdNGkjYs%*S-nMPXDFL{vIHc+FS4cNe2>KR@g z&qsT}KsBDnsh5dEwlXlUI;(N7p&1uYk$SFMzp}cp?dAt$e#K_=m5p~5uGZw*(N#Ijv1Vg8O(ZHC2 z_u_M211qAO5v)XQslU${!_+PkC+DMF&P8Zc3idrj@+%6D(m zav==EO4nRvY|@}|>-!n747-C+e7r`^!J*s`{R;uG?MX-aqTt8u7)5h!Jb9_nY&1 z&KI!mPB+1KzmtiK)2J$72(x3AkA5|g;{Gqp-(y3banUkjX5JQ?6EtQ0fYQrRRu7q} zrf|8p8z=KY@o}d~bdU50>3gN;Q@O;7Sqp;{N^~?Q21eDc1GRL4LAu&5rb0Ji|lXq(32ttUeCh%@Ih)KP-o7HR6=j{096+kC*gCP4TJrQGF9GR zCo7>nv!z`>Yzi}_pM_%}ty^SxA>+b8;0lNvjpb6sxk6Bib~*Aa>!&K22~wWhs7XRL z1~c13pn4>j;x>*KahD!@RNcGNJV!f*ox5@J@iOCIjx1k`sZxbZUW>GkPX>+Ob(oQh z^GQu4?(;5xCR!-S5nDNE)N8W+c3mK{Zia#tlX5HAJo7>u$3_B7D6V#F{~6>`MCVEY zl6DAQOQ*1`H-&$vN1ewWQ|${D0f*&g^tyNz7*Y5NFcMJd)ENLkDUfroM;)t-J+)+L z+bZ-p&v}czL{n+yvh>vDet!xV0~!Ny*OaV&b>tszq;l%AhSHO4EH;1zzH8k-bm$=s z7D=8upSW=*9fllVk}c>*Q)7)hZdo-qhnw)KT>sno68aUdxM{%O=@q7w?=-wA}21RPI&XFX(SM=LeKCIi%WaW z_M+Pl)DTBM;D|gfN#ctrP6yxtHF_Sm+3VlVJiBa7MhJh*rTnx% zW7WH1+x~EX1M=Y(6%xF7WC_7*pw9WMJaaX2AMpT%XV6MP`C(IMLE>eUGkUlxj+HpO==`*FQ;XNaKt_Uqc+$x8E}8Xy0nvQ*H0qb{i{NWX+2@jWHsG ze2A(O)YU2EJ;o)Uv^O36Kvt5xi28(TK-=cjr|L_pSzN6cVpBQha~I*BRb<+1>P140 zG7?es8Vf)EJ&A)G{^?&A8RKD7z_V-=-Z)j)D+lGdc7@~O&2T-ati7d-Il2X@t9$^t zM#yq%n&x1_K|<&Y%%mnhA05f18WoQOngLNeiYO^?|Mp&gTYc~?w1%`9$5zMfrmZV2Q|Y%IBzhO~%Nbij${GESSa_h(Klus{3djD)gH zemCxy-p-49*G$w${zO;N0ey7FESJu;8POV*73?-<_a@#7eO^cSymzzh#M6dm+26GXj(4Kxvza4gdfDJLW_4^%Kl--dH_jwusM{H%iE1_tGRSL7)LPNTc8-u9JP`emPN_*Me*)G474xL ztlGRc9HX^+@{|@Ioj4>^Hcm$Z0s!+0u=1X{WC&4G-Gal@pP0-z5$Vtg>z3GDi{LO} zToZWe)H#G#6$yjKOzYg&cxMooW|4o97e6_tpI6fTT0J1;b11QGQ0O%ZSrU2ZgWY=J zgS^T^emXnsnur!w)dyTfYDs&&S3Y3c+A(`7<}b_p!LEVgMT5kO#Yp@8VNo;@Pz)Vq zvs>`#%DJIIXHM(TG0_BM2O3IYq+>wVK5bL2^GlO}TfEVSaRbo4su91@l>P0&)!dL* z;Y6HYsV!aP&E*Bu$)eXFaB2?i6q@1}5$xT=r&L*$!*Ye!<42;(<9HsOK~Mn*Rpn7DpJ@LySd zb$xNx6$W~A#iS|u5W1@5De|KtN>a@sQWWN47`ep19(e{+O9~BcKvoT%i;_sg(#$X` z3rjhL57rczGuToEMz`thg%s_;J-^IjfaqKgzUb72amHg((tb5kV6PXI>}r_*`XrcA zC$8BKupN|RfYXLyf{W6@f-49N^4FvWyW*#iQNjs+6$NAXJsDUejf3r41TS`cof@sjA0!XAle%BgOeUcW-7j6N>2N>lyV#&qi` zm}v!RK>>=b0>QmmS^m`NsN7<>K8BM&GArul&P~BWUK@lkW2uemb9U zLEgbM;7K|aZaIl%_W{*5mGY3mHdd63C4mCL9BtqsBe8yw158de_W>JsB`Q^^7J_}c89y)G{{d>-`=TfImHpg#UPB>eqGqTu*a6 z2y#>&3G(xAHy#wtIz3(VXncRVZ@a(K*Bu0Z{Dnd!vlis|Q<_ukdaKTHt!^oF5tq8G zmhLInZtk#srgtS%`{T2|OV@`alF=9B3pRCeI-96@nC*%&ie9;c&TNl)I*OB{#8?b7 zm(F7sy)+WZxkD@v)-QPYT)PC1maJ8ggU;q&Eiu5vtKNaPho;4F%*L zIkm%(r3M0&5bEn5Ata!@(xy__X4>sMkESU!Hu_^F1C<{1$cDCo^eyfzppAsN7pY|K zg+z2~(h^Zr5zVI3>IS`8AW$j{t0fxIF=!c3I& zj#rz;x3t6CJLdQSxU-{cjfUuhw7rTl0 ziZ=k!{q{mTr2bYDUhgh?Kk+H2>v3?zPo^8e2eiXps#AKYHL+|P2Ut?6I`hHKAdMx% z&Y&^2HB~PP7tty&|5?k(l2D`3kHFk!*JDg?byh`XD>0{B^ulO#a2!ZMzalRr`Ig$31*$%|l6Q9PjXRFbXb%1!8l$BSB& z8&mDnDG%uXgjDQ~6&|;?-i9rmmP{u>dXDOnY(DCKN9{|X^}PQg_Wnz6`Tsh-{hy-t z@5XCcg1_s+L;Sqd?TzhNM(*_5+}yP@+bt4@+K-b8LMywHrdntE-aj{MDh&)2Lgw%d71TzJm^` z24p;WKiEK&%0?~Q=%Tiao;q$k1fsd6WyCX1qgk^Gl#8P2qfbgt3p24HoL;YWWc6Za zpZ(6uDv^9@lsmQ`momyWnW&u&mCn$slnjyb+4a8uBjl6GDfykOSSIzWi$TlMseMC> z1V-0L3v2QB$R>1Rw5U#FNIGlhfJtUhNj=X@sjbszZfF2 z9gTnR<1EOxVi{a`;RaV4_A<+$dGr)eLURtcx2@Ka69BkjLhqqWLzN^S{s5F1C0$oV zXXn@Y?E^m37S(fqaOt+e8x>aplX`uWPjxMo^wO0$tftD?;i51~{@gjJ26rS1v0m8D@>zh1rSU zC{%jY!?y#}^s?H6EF!M!U7cb7xEJKx>kmQhA}JcVR{%|HOR*ws9Wfncm(HDlJzO(_ zV3&lksuBO$0&rXr?@xI|lA0b%2LpQA)JF?GjCMcA>jh{YzvQNPp=Zd|W{-$w9mW;_ zz|aDNl9W+}QJGNd0t~alO#x_gN3&cSO?vqwF~Y z_vX?92lv(bK1D8$BE_OoSulU5vsd$DqwvV+^?u&N46+IKfP#8x)^eB=$Aubf6d+98 zy^Wxlr3;8eXQjL+W*52upPT4Q7pL%CsFTC5OYNmYyjGaMG&Y2M? zqz?NR_kG(ckkXlEn0^AT_m;w{4ME=}A1Y@S0@yTqCJV7S6@nUsDqU7!iS~!720+kc zaP)GSjJUzBI$I!C?}T}@l~?n{&4v^2$?-L&s+9P+w0e_BE#OMiX#5<#1|+TbgVb6_z7=K(K*6U<`talnT6M~LV;1sykjOlZ& z<8?g#LiWKF;y_oXT37CHO+7JhXCTEkYge}J3`d0@NgdYSybTbvcDhw#jS|k-kheTS zxdE8womw1`y%pql9qH{_-9v=O)iQQXgWhvB+_2fgD_=eP#o{Bf6vdJEx-m=WcHb*L7+4$5X93HMMX(k7U(>D92GfW*`hCc$0m!%+|aT##K@w3R$t52K`!cpOL)ZwQCLq{ z(>+i{b30zdkZ5NNn6pAl)~K6fOisn-bh67oQ2;>Pz%Shn=XVgYyhG)hP2i)w2up7< zz}O-fZth9DQte1a=pPkwiCIOG^>%Vd{EAEq-9@`1cXJ9)N!~xKIFCC0;*bJz{O(Py zS}ZaM^;-7EzcIq;4u^+THd*~XWIOaZ;=)HZ!iSz`PW#Cb)wPaA*7ZUN%Al`^5%QQL zB*5393BVQCny)kFid&CmrN+MdSn`pSk*}i^m#4LkQ}S?`Y7u@wz@OgVglQU9hEO15 zb_&7C)O*PEiP463BrH)ZXNbrdK9Of~lg;W}>+dzhaXu{~?hT>SseGXPs_w@p&`yz> zi~=bS84AQ!Fo1?+SXD*;(qrB1=PLJo^nvOujwcY33A6*1nZ@0ZQT5c~BNHN2vIDOx z(d~8zEHqmZ%G`wJ_$~ComM^8Hgos0+e(tO)AnvfQ+4H(H?-PNOMfI9x>A?!m+r6zJT84#s$n85ZtC`AJuX*_y#^YgtkTF;kGZ>9>B|#VL3r^NUtazL$r*kjnh)BRK^Bq6jsy51fe6AHpvfO2e?EFQVN^#!&PE93Kpbuaa>mY4Ez_p_iO`sXtV{?ERe=~M zAHvgDr_i|vn$%z2SE50;fE%B}8gwvfI4a2)cA#l$RAehqgZt|OQ2?1T^;V5{^;Rmt zoOnIA>M__fEir~_&0ImoQ0{Ap`s>7fqV+J5Pij6@im)CuiPjn99WoFT-b6d2gx!li$~FW`ipx zpHr2PMQH>%dDX)>rVtZto{i@L%t)PV#u&x#GduWEU|#M) zE1<_22iUz(h+mCt$2!u?!yRoH71t^m%C;f%2gt6s9)KetRu=+U8>uF)&~rF67BP-; z<#BMXuv>m9#Bw~fEwR80O&@V0ukoFG(6A6Ubun?ob={&ToF;keNGU|>krpF%kd*JB z76^h*${yIC+qMo%N+F2UZ979N#F|2g)XiXfVG5mM=uDhTAvHe6`&Qa`WnTBO;Cf{x zn#xt^!i;VPQ8G`5&)BC5bDQ;ViupSfPJVhCDS*$`oVn-quLqg75hP>tAv6_*2=cG4 zH#B?@%82H9X*v{8AZWd(&Xx{amZ+BIX59y;{^RZ`wBwd8F11NlICi#2UDG$H4^dr4 zYi_UXCf={)ei%QI5x9iKLQ(S}m=6k><|Nru2s&5yg!$#rN%C?SFr0{?B*@jAn_8Yv z)a%+gl9rw(TW6|39(8AoQpRH867d=A>$~u%(>(-pY=xo2Z(a(UJ zReUrD*Gs|~U7%>mTxM_4Ju0w13D051iRyx$pEKUA75v-)^w5DS3eMk>x#1 z(7uE{0$U|x7ZNx$q}0d?sd6x*x&b`u$?s1fMxXgb;hpqRsFd+?xJx8DBcB_9Ibxt# zMb+^BTbwBsok8_A({UHAxc8IOxpSfxXycgjc}*{;hmzs}^9BU%_JaLb-+J0g-d9;f zI_y{*Yy{1$zZn=HwnNkc8GS7Se-Y9etg@EC5e0*Ux-lc7;ZijlHo*{a8TQQ-3Du%7 z1CY_hcmNw$GfZhermtVbgnvMP)*HhSM-b0^b)72*TJbZBZ(?v26baM}2q1``I8ZDx zv0-#Qtzpg#p+I>`&QRszUh!V<74$`Flz&Ikg@3E|2(DY;*(%Hx{W&MeAmCbwo39kp znMHL%)&0#$R%m4(ED7;5{D%;R{22Hi9s@rzsf6{q@G$Ofj^>c#6WIY!L=n{+ZRt+R z8A&zG)*5Xj+A!kU4_}U^YEaPT@kFYP1D7lxa@+Sn=1>3rZQ-5gS*e^8RJnBun-hQg zyl)BNL2mJ4NH}R?aH?!MkyLw25e@;uN1$_T@z(ZaGbd}P^Sn|W)_%7>hVr5*wTV9H zukn3i(m1V{7mUhSI+=5v@|RH_CA&tciB1Zb$o5yR_0rDEp`G}?KR5ac+&lp{ctPq0 z0dOHs+zCzr#T7tzh_rRN^k&H)pm3cVyLN|XF#8044tTMECWx%0}>%m>JWD@5It+pwr{ z%(vq^dXZq3QHE9-@r7fitcj6<@;GJ(_=LKl7|`%$!ugKmc{{AZuUtp$Q?*clrxts5 z#!Is1U236*XP!zADOqroaCZ<;1aXz8$AaG+fH)SoXF#n5D!Lm^uGQrZ73`wHA~n2S zh9(M+lecN=U*x_HhjdpTJiSsS)yzoo7Zho zZlds12b2nxRJ#izjv6iLB(&Mqb}}?mH+fgII^!GR?KH0f=!g_VqG-kU9AP4o7(fXg z=iTZ%uK;`*Q=DG)VGDhh=yf#!kqhse9fDyq!a^c9LK68tJF?J5=U2nE9aTV-(vsi? zl}C`9Q$$cfTV^}?pkNWu)t4Q8&%QM`H|$O@9wAvO+IIKD z6uIN8Tfk@e+bvE9Q2V9OyIKo&?ArXKCNh<(7Olr}W<#!*gnd9{Q~Zhi)p@P}V8_(! zD;xdp0w87#ZuMux6^3V0uz<=Wl8_zO$A7_6btBK7Np;^d6e_Xd^du+lCaQ-`HbCX) zAo8AdNWsq~e&GeAj-Q!;nW6cRUCA2THu1_CQ}^QSCCJ+reFd>=6Bl`hE)2!M(1+kafhU_0lP!w7wc$C*dd>x9$3Xj>PX<^tQSH8QFK^O$42lQ6pELOK6x7Rdoq< zF@PJ{rR8UNg(KU*w!&*2s+bp4&9}Ec4zKYd3Th+sg?FXk1hKA3o!P(@F9a*fx4Hrw z#CjLHz*4z#nGJr&C&?u3{*lvAj7oWbxIX!7i)!qP^uNfh{vG)1ztE!kzq9wL{w+8A ztGzGf@Af{nSdgZFvGBiRZT%xK`^(9E1IPX?Fk|@(MB={*K=!v5*#Dcr?Eef9@lRtP z3&DQ?MEqAeRayQeAmXo=<$s-Z{yj4LmbL!}Oxc90rY$x*n$L3W4zcNMzT1%zqkmf+Im;KDN%LK4yd)Ced=udL2Y zyZzyK)DIO&%?Y1Oj1pF+Y+CZ99pEG~U|bxKIR~=n2$G`()Z&tTZv`l>ZIt4o$Vx!V z!6YT&m~2v)*kTdhHUwZkrlb5cdEpYG8z+St)b<1S1~A(VbV+|rD!a&H7;PjiqZ4C7 z8q(*(eHB7b-WP_ri?jwJbZ~tnMrn*3Hf5ra3InZB_CI<>B(mxzG(%+C(}h}CgE)F+ zDAEqw67I~I0IfSj0aB_W)RPrZzYj=O7ZM|VrMG^AuSmoD-J5l;SKESdsDwy71OMs~ z%<;pKw)SzAs&Ws|yr^Iak{m6QUlDU35@B3n%zW=o+5|8N^`=*xY?pQLjf7L z-=YexUyFvYoEqB^r_ZJyXjO}!wxaJz&W4LWbG7vRh!dU4`82DCB{qO>XT)p_npYY9 zsZ6KF!YEQQs0eFyb>+04_0G{?HRpVpvzN zOwBK=z6G?Xcm`)7Ggjrh)WU~b%hYJf{F33@ZtGmkGTOWXbK15h;;)8xGlmPQZ?2akQh*benNX3*g$ckW(9d~E0gyg#IBWQ zG{vJ3&uVEUe{oC;pN-}z0jQg8Zoz$AG;>$FnITA810j(!V|GvNH=U&U#SZsT)b36( z>C9s#GgGe9pRDf~l5Y3w~%+kVQiZG^PIrGMjRu* zA~w1ZNJE71sD9ZrE%g1Ul&Q@V1O#FMB;$j>g9uL^%ZJ{b1vSZC(qr+K#eSxmc?V!s z?$L?{JnN+RiD^E?cT?74_rZ{GPjh3tLN#>a1Ik z2=`Br@`qon2wiktIBY!H&ggC2%WeqHJ5M`dS5!Ts-uUsaaFEPK+dv%ay>kaC6kAW(s zdp>oj36GvN(&fgm+5~^07Z>+Erq=U8fWKwitweS`&bW#O<;Z@L_Wq3_Q zcRK0+jYg_b{;vfoh=O1}!1vxU1~oAAzgY6$jcNU}_WW-&NB{p-d;W8`_Mf%qe>xe( z@-HW&SpMBe*}wRY@Ne}d3oGkCchSYe{{kfYi#uxhFYc%?Gc?;xUI?H82^Z$b$-Xqi zzYxhN609uAe?m9o)PDI?ia%(3r{hHfN+~B!Rw$+R_g6l}4sTvqcpKSn>vZztseX&3 zLTGEzR{cLboV~L@Vrdga%=EtdtY;^vXtl-+u%XN6K+r$l-yfbmRvId&*B4FRMrzPL zmnTncT6A$;S93Z&T|aJsGqveWRPlXMtUd-v4CT%~&MoTWU{-cr$byq%XqT*Hi6_zM zX;91igPC9yM-7l02m7r?zmV0tqG5caP<$g4yCMy8E{Nr`{KHtl(LUOrkEG{(5}J3r zDGmw-eNBI22xro+*^^4lX~zc1UtC zOioIs)%;2)LO{xElXHfr6*wCzkw3?r_t5_SxyytRen2vhW|U$uiGhwtjm_P2MdX+= zJ3RC*^4or2sKbT6$g_ZRgdhw}>fnh*xJ`pFe~-eKpKIz*Q`+iV&(-+hcVLWLGcQCo zVMd$)5{N|o*qXihY5jNtoie2&mabzuZ1^}+rfePD*Zo`Bzl)&y=jnFiIfrt2WH40R zsdE{lC4j~PAt(%iQs3GTLsRYm5ycaA2as|YQ{Z0l406}kPMn2=!JJeL6*2*;+DuXOAe*}&-QhhheiW(}3+28M`9L4l2R zuZQdM6~(IRB8NMFK|4s@O?^RIIhL7APST`_YGJdt1nL0pCbk>+{vFWrADo%&Dj_{E z-LPnf!Qp*CdW`6}5PU35<_(HD7fSjBE=E8R0zk$Pb?siz^03}Z%3r#C@6U*?9E((4 zwQ^U9&Lf)uS0Q7CsG=)zaNS;cCmrJue2j|<= z?=J$v9-ksA2q8XPo^C6b>@2s{hp>Xt=F^Lvj&1!IKeQYp!jFP7i7?{8fk__neV1b) zYy%19AZ^DhUw+?yX50R*xmW|L{!W9hJJ9s^oIc+H<8!)wP=37nJ(p**=sor*pV|3u zhs)u#{5A7EQuW$4Bi5EDaMQ87Znmh;eb`(Q%9mX#TO^Xq*qROk?OjMGmW%=Hq6CUZ*Lh z0YwHknN`|~@&MLW^pmSw_CmZ<@t=K7wALBRc3=>?Z((Dz>{79{Yuj^!_wq-lU%y7Z*s@E2)~0K z(^a6x$Dc}Y!6m~@5{!HTnTmqVx_{ki@E2YX=}f`rDXfj9P2y^< zCOGwC--Oz&CMWzs8iG{e9tjYik+VgEaBDh|t zR&Xudd#ExT4ao{TKIV*t9>`XfO4^r4j05ykF`o2Cp9Q|-q;o+{w;_Fjwe(pn^4Y_K z={x=?7d#QiVp+JSPYuL1NKidjH7E!iu={-6@d&aX=Su4JbkY_2gxU@2EnH=96$I2d zb{GiuE;RTV9B4`pUi_^(vx|-q=y>)R2-+?*uvn%dJb=cibDREjq3Q=VYESJ9z4`)5 zZT0ttG@=$%A5OSGy-Js46pEqN_<*bI0xAJj4u+VbXHSgkSE4N&KZIKy@LREF9WJqQ64;@qu(L^l!V&6Zf%nN8T&%v2csvJQ=nre&>+z0#g1y zvJkKyEL;U`s!N?>0UM>RZhrq(1yOIIa^*nm`1M4HF-Io#$#5`Nn)#H%bc#ye)vlf< zJxo&S+sA@IG&_m_XOLq`adCzJV7h5Ni;seL+Q^sud6OO)bKZ8G^l8VM=af+YUZ^1U zi3*^wMsXyDS)op>i4LI6cFtMWOIrh0B*m2y*Q5kQNB=b}lx&||HKhyYB%zzy0iE;> z#Y2!hQKOnCL-hbkW(5j8_<9e5?RxM_s-tx3r;v7FB?z9Y4=VnJY*H{1s}&`&d_?f*Vj2CI6Hp7fml$c({{LB+$jxHHrH~>tj>a!qizBy6;?cLya zk`h;}s|8PiKT~KfrcnCMc_N%a?zv?v+aUH-)-s2)4BP;IZ>AF;{_GgKr^JEnoRIjb zkDfD7e>s$pdEgKW1 z-g>DcjLEnBZW$>x29n#W+S)f6XHjq12BJk(LUnuT66R2=Kose*c}e^&dIx-^CCA$YK8u z?)~rJ1^nOF!vANyfWL78{>x}y7S4ag2w?foyG;MBS&@d$K+$LrGS6pe9D|^C9EZ{jlU5<6 z@pNtH_1Ussy*+5&vU;*=D=0mC{j4vKjkWIgc|AFOVQy~KpiP1~i)=Af3=CYW5`hhp`hZ{Powt=1(2vrQg>pAf-M25Orr z^34el|K~>QolUIJxRWX1?*RVl?JwJkbwTGlVs->FNT8F!$e~l62&qI&%)r1_DOSz2 z)o~7ydEtq1{?b6{$ybI-R_RPduLO;Q<`Z#9Pr$SmQ3uipHh8<(y$l_UTo%YY0FkI& z+pGUz*e!1s>g8WS15jr4Q!r9NZM;8f=Jg3Lu1t0?#dVMu<|tNd9kpAsgP^1y*n3Ba zc|^ayUq~J6R2{KTH!jE$nS;_KhzfSL_f09vX!(W&4Dpjw1=?ATgrH!62~I!vBXW-W zDE(wgHah%+N=fx_8?4|aq^>nGyAV6{OAr-}u>}*%j|v2* zX8(a#-sjA+*Zn7zp5c*p%6=I=h7EUX^Ld4EII_T>4Hxs}zFwGaN4&WmBDXjqYT+2k z05a;~;?Y4)2D(8ZYRa*Oq(Gy5D*?97g*2$z+)fWTHXiHVfJZbh%18^Wp3@r#Vge1F z130SSS;Xi0*!xxv!Hd*>52Fq)D81$I{bBIO64|*3-<7_3e;74|G=1$hfmfcZM<9e= zEMKj$_cJ@$)$GI-eWEd^U?myfs`gmpkaWoy^B|6W&JrrY8x{##Q<+ThQFb?&M~Lha zeX(cnydzlIKgYyyb}rei=uKwu8(S{)c3|=5c9b2(uBBkzFMC1oSNb3lmD%XG%Xx@i z?ZCpZ3kQ1S=V%5*A8Ww-Gch^#aNeyCJ+2x8++DHFQ6;j+&|i(XgHYu9Lh~W(@ZmHZ z#F-zaX_fMn$Us4CPeD_H{x8+i15ii!B;)n&7iTz2yh8V0oa_v z<#Gswf*@T(W;aLoFQmIg1vG^7JApz-x|oA!ySg>h>D!d779Xt`Cxv0{&3L3W&JRsy z*=IxM4p3M9g7thaC;;t zj9%XEp~WefW?Dg=D^k*ZE?v5U-AvuaWD#*KyBdp*QdvQoLO5sX^e9dzIb*NbP zBYHh@sywAByrh`lwAEaHd}Slp8des%&KuQY7VjB*yQ8A^RFZJzcNUovW50@Kx_OOg z;$qW_!;}{!Rxi3=E^=|JFj^&5KgkEOV^YN1W#<*CoR=8!Ojd2nddG#&dR0rWDe6He zE-XPaV@)@AN> z!x&ZJ<&_4M$@MTU792P(q3TB|0(*WL7@L^08A+EWMlvHrG(H{3Z(^P?t*ru%tWXDdr-fH|r17pdaBdnPrmn$>nJ zHe24Br1h*ztiTFv%H=Emt4Lt7t6FO=cB2PTS@DjW%N13P_Pv7ql%^!nFdyi|Hp1r@ zq%rx^Fd6NUr%G%jL$Iy^83=;M;<>g6C9;4>bjl!>?L%Cs7YiH|x}}jZ_xzKcvQ7r# zceM($dk7Eq3JQ9%GG&7;&$pioId%qIY9ILrQHlwU7sJYJGZsW@vqf36l1wZFR1r(1 z4Ovhrl+o;-{koe9IgJXTUHO`bCksz_Uj&M)F*YPm7=q~ZuAKRxMOJc6aJf(6_s4UK ze%hiO5y_T5!*pZ=fMbnZDXp?NV)2DI9e1hWAs9@U&7aR&IpHCx?V+rf^=ruu(7e(C zY4-s05P%_AM-lK8R95;xzOV6|^TLww3veQ1Al9y(wkJ?^{6fIBUUPYXz?WcDJ_Y-a z>wg?XhstP5W}lPL%|KiWV?^l_7Op)$dNPW3-B4vH8)1TQu-QwrUCR3nZA&HVjNuo~ z;lqgn{N>l;QE_&u-u0v!i@*bzu0`!lqAl$=tC3NLMGmuD)jH_I8ZF;#)@k`f;Fk9D zI;DzS_xNlCGE*VET*;$R7&rKsvBCX&=5gKD%ngbw5o8&{(REH_)r7Rn$s^|7;rEnr zLYVZXREk8qnDp#w?`||&CJFg|^E*x+7X(s5Ij1a6f-#{9OE6Y$8~P8ty8kcZMmJ5qK;1Lb&FdE4UK_=|G)OUMV?d0iiP}6wp|sg!1fBg!~b> zh{@3!EnpY(xLH*hX7>xA1xQ&iQ2-h`Eu}iRzbUMqHp43hgXGws33fYeK^z5p&ySL?xK=={ zsRj;{M^#Q55u9?xPD)YxWNh+TVTaN#RyQIbIdg^Eng2OiwRg6oCgEf*^vE<&T*7EN-44#Ex*J9}6(a z=WdQDHo);t08`-LUtUdi$9Qn0)fCtBP+vw1M%Un3TOZeh>gaszk$;o|PYSZ$R&rM+ z=fc5t&n%`M-mO}!L&;=7C<>!xq+OfIOQuJXbIDVujv%e+kTm8mBrHLO#2D`s-XFfV z>JKN}UN@tFZ-#~{uS|LPnfa2B$vTpbr!J%wWuS{@f9@0nJ%;fm9b}TwKJW2vLByus zzF2C<-f}7p^z4|?Wkz#Nnf z7TmuaMk)W0>wE>O?VfC%9$9%Vj(TU?<0CfQ`+D;kx?|q^BJb47z;4m!@VP3Tnb0XK z&qmbsI4@3hDJjkISQjS89=9ip*?%a^yeOp?j6$TGlXqxE z=iGZuR*Km4_Os+nd!bScVw7^kEu;vo}b z0Xka_yS=WqW6+7&bnUs^GQnI~q9lBfPpVpyS*A@CH8zlDCFtB5`4%Nma-t5+*_(Z- z;199K*F=5SG@F69ab{mT6|ow3E&HejycT&qnhc|!_sK+7A1Jf6xb?NpT|l;#CdhJU z^0f^%p^v%Nyvaxvk{}A@Zl#ClZ?g=L8`$)xP4q`9`u zWcufsK@UBgd4$&u!6|_s%B?f2SXYhWNTDQ=9L*!AR&ji(R$&CV5Ru%-78~Jr@)E&$ zTio6~lG^Lk9s}T;Y8~onmf6>-z_>Ac)`Fx$Bn+; z5EY{+&whY&Dp{H*njDm&c7CT^nqTsL=wU%2nZF!*n!xN(-qN0b0ZUZy4a+Bzz&q7A z^xiBTCT(u zfN+x^rEr`^CNpvtKeZ1!@-56HjX+WkU7|@3k@ZK1$HLvAzwWVg8)gIlY*_5#KA2PX zwYK18=jA@K0`87!Gsrx*V=;hdhp+B{jQt!5Hc~fr7@;`iR$!EuVPmBMcI^0UV2`yv z57vH3>bJ{VT~g~=+#%_3vb$Rc#T)e^*a5Y1o!-$^U)OZK+B`f(Z*Gv&jZ*J*Swhn) zRIn`tFRexJ0!yvi*t+4ahyNV;KAlcqETeoc6_A1-9ZQHAC8>?*F z)+*b!t5(^zt4{rT@9y5`+hWCQwO4Oy1-hIXtf$!EYhSM0-m@0Js95{U%oZ9}{2WkS zpM^i~@bcEeg_9E?pxbQa!J~V8+QES+d3^rq`R>$)duXnURz{zJZIBL`RFL%{(vS6GT*fYXU{%2*A4a$ElPTBq95i%){%O?1ZbRhi^Xr#&VMS zjWZ1bv1!;)@mleE%E_~Sv0PzTXI7(^lYQ|txZ9>(A35*dWa69M<*oIhkFG)1Xk4aB zH2=Lijn6D%MXeiSdOp6}Joq%ws-NE)Gt{_TImz_NLRUMJsnb)#emtC9f4W*3U4I!p zX`g1$+Ne*hTOT~!cjApnGrn7tpx$LSxC71Dk!*P385ha!39ZxGXONN^JKU0`eNGv< zWn}b1r}V)-@WL(XSQA#XCle`bHJ;76^UWHdpc7SiuTYd={+bR)9m<_~xea3SLWDMP z{vdLuv)*_kidJ!C3f>J6LVq`6PUu(>VH!kV=IN9qnY#~eqEGOr4<<0Ad~*=Q)D*As z6f_}b8oY9@cnZi-(=UG$x2n|uW0h**_l)~M@VMT3#$?)vr~xJ_fx7;!Y3a%GdV!o~ z%Ff)|G;OJEc}Op+Qam4(QQgE$2o--elt0EWIP4?Jx{`w9A4P2*o zV2IC*SrrIUnVGO#Aje@Q9}+n-T+^mo=-T*uG5Cfl!%Qt8Krn5?w=f?rdm59xl~)>5 zMK`EjCn8`xf4DOOq3tZ?jP(;+-Sw>PL-> z<_y>cncI0%WcYZHZrRtN4A6m+)e}~1*M2Wg3u8}ux6jh$iqLFqj0&+a!Fo(OT7U#o zoUt&5GLfhr*Gyz1*0gu31Gh}?=V&eon@R^>bISozcYc%@ER~uwixW5^`^QTon|)O7 zcj_Pm*9wvGLx1}a0c#@#0XXh61U`Mep58oT)c!vr=!)th0EP)_JQ&N z$7b5N_S{kdL0e~;FsFeADmy;nvO&4|0<)aM@#$NsZ}zFd=>0qn50#e|NOmH((D=q3 zzUh&)rs&i`bsKLHK}z7Jkle?^U6jd7l4pZ#XILrwL;v;@Y3e2I>Wj2K=EPQ3%5y_H z>mIHe+e9GU{jyZRzu(GjBW({I3-~F4TR^T8nYraGV8maoh5k!pZE4Y0sup-p(~K}h z-Kl%urHO-ZL)udd|Mqv@1g}7`%e-tPn=W^7`a_;6rNuAvf$AStjM(hJLa@(C1f8a> zZ>@SMsdI02Cc>3mzcFUr{b^ObyhR|n4+TWe4*j9`VUJ&tKvH_xq05jBw-Yr`}0hR_u0(~XcY*^JpnF6|rT(L!o6qOH3 zlp7;CYAKPR87Cw4yBBxhYg)OKE!75)(=3YGat~2}MoH{qsA&H&q*|pem1Y{DwR_-E zKVIWVlh-d5fEp&X^;be!n`6IzZYg;E9f9(K*o^i}!Db$dZB30QkEa!Hb3Fi5RE6tY z8Yyn28_sKO?pB!|eFXOF?9Z^#6du$G`yG<#Ky!wM-g}9I%4Ls}NdehK&|^V#t^9g? zn?;(@Gg=Cp_+sz%uH^j0!{^$wW!ZYW*B?RlS1aRBEO57T9)T*1$Ia}Z_BH>Ms0ti> zoQm-2=M0r&ntS$k>i3j=H&GvITYc{%xC>lBKbCdX(ssX+@em)HI!hDW-~$fDE0}=0 zj*|oF`EkrsIg%D}4}cuMUT?TTqL@1gs3zO(7v|twV3hhaM3;`iW9zYJerROZ5u=W7 zi2O#u_9b=BM91@*I9evC`Tn3fLOmc^^BN&8+023*7nBPmhl}h_ZvA8!OTj0@)Y&?C zFjpZsQeXHA`G`FW^`JyliNp3QAFa?k1O4P;tgj~!7)Y9?COfQ&r0|Vh>(EJH7>zH} zjLSrprGzJKa+QFRs;aZIlmLfCTwd)Y|M7SG#zTKB=&0u9xr-B_LYc>J7(}QlPlbx* z#N$bUK0G;2+Pw9epLY!2kDo%R(+VCf{t9z4X1XUp^BZvZR++f$lci^tcgW2crHu}A zYgx>ccT3yb`l*c8@ewG7Da90CeqD=nF}6f38)Wvxkj*C5BCO6!#Ln$&i}(6l-xOzw z$xzobsRYCmXQSRn)x8Ye%jZjdR>eSo#d=626EIt7Fqs3U*txwOOlZ*CUp8a=R@=}+ zhcFSNY$OOS*{V08Y{WaSg`O~hEW|m?lH|auq(*?yl00Vcfxd<IAwW2gaPVM5@L+WI zY*|#lzEh*;MfzvVy_$G^L*d4js)RrM)ZY4`xXUMhxWF52v-orqGHoj^F1~W4@C2l2 zF6&H!h7te?myHoHt8-*O&42$DzEpyYwy)jeHl`h~4*g0vu zkU4q1=28~nVDm|vy}Cj2WlYSr*UjfC7S;<;2AuyOuZUXDtYqCY3v8o;SILl=bfc*S z?X-rOwZvV&WgLEdeL?9uQ9&wzmDS@7KSE&S^wmTQMMIvj%?sEsr^JmVZWIKIR*uY> zF=26zi>e-t$h#09EJgJU!7XTk8wA+LR2G^7*pD5WUM}GGzXZS;4Ut3tO8l*0k%=H^ zoMIL9+w2Th)r%T9M6Z`=*ahisAY_LeVL*)z8gIl#7v@dgBY<2eH*qp0^%HvROdXd> zIzj-l){9z!=L>7CSotah#Rv07g1U`>Z~OffxEGQ(11$i)!X}PB9yO5+vnv9jvHJvlqBJP^=DD7zK3bBH3 zZIzllcTpL*j`@udq(u5;3c=5YnvXgsZwO+#q(3^eKlB^t209Pt%~IycHt;)$|w zxkc?-xVZrRK}i;dVuBU?#l1-=x&dvN@T%iHF{N3BT?7w3ndpKK3~@ zlonV>7i~&AJT0}=M=&X)6-y<7l30b9SgmTg*$w<2$-Stg4hPy{tE91%kMIw#NIl3Jj82f&!6k3B$rFX5f9qD;eMr;kIBNbBC8bp9jAy zL~b#%7hjwWD#0!C;RSf@%c3Uzu+f0vRgGPc)2$MI+ySn$@dGgHaEQ`>hOwWYESCA> z$-6z?22-8wG$@jWy0M};=jYcK((nCDKIkWSq)S<%XXq%iH{2VKK8Y(MFS$o6k2yI! z1~S57*rJwCX)BB96GuwgeeFKxT`#FXU9+&%H=0Um$9Sml0Z#RN3sS83mYymYnV2v^ z1oDVZ;!k{D=_TyS9Qa{>ki|a|Wdu8@8s&>lTp3)d;m*&lTcv38r0*R^9O2wkN^^NE z(ipmLKj0E91pM<$-ukU|iA~^~Mkl9rMt-pnZcvX#b7O~iLDe%)#&7;YQ;LpBP$IB) zct<50jkIu!HpC-U6cgcOAsUn@3=n%kU@C3#owQ42OjWFFRs%Gm_stRa- ztl!#OkGEYO0yo8+O%OERCB~{YN0;H5*+~%HIQzhL^+0Ln2FbE>_9x zc^^diSbCQZ&f-*PJd>pnPZn#f_lVF!-eLrdze~W;PAu!A^oQA)+wFyOIP=n5_K+zt zR6`u4&786v!_JFD)5z_`nw;G?9kWg3mkgyo3P7AWUBRrH&^%>w-mx@A7qy+}fMIoL z(L8lvU79{U&NeF%h9?hwmMP}1aS)}&V5Cz3|FUZE;_vHmMxwI|%R=)OcmfHDelgvCs)KHs6?3rnY%C!wrGHBE6=Y;{7%{5jX^WH@eM}>g z^DtFVWvxTDZSrj0Rom`FXV;9U+PFREl7)udXzc5yR~hRmM0AD+5oOBU4xGG4S^?C|1?LQm);PT zL_4k-Wt{*jCdrIS-K90cI#)}_&aK2;jsV-a>SO_1FEA4kuj&OA&;R42C5od*WT@a) z?wPr7FQ%_+l7@L#F*V)>`FUjOpp z|LLl8u!?0#v)(`J~M6@|E>wGjx@ts>@rJc22Eit;glncGm+o zmn0JZWNKAT`E*QFFj3KvK{B`?No2Q+yFKRz?~H~9-@{eTt-1!%O&!zb45A8FlisQ6 zt&hiSnm;t?W>yL66e%0g`gAo2pcgI} ze+Xdm^816*3674mh3e0^*5xp0G!4)3w*L}u#x{fD-A9k_uro?{g`-%ElQ+80v*d5$B@ION}0@eN9NF zmRc(xDi9B#RcF}lWq&um+fOc@(U3;st){B-LO-Hp1N6<)(BhXL^BdwXEXyduAFqE^ zNTy90GrTy;4Y89fvP+;@LJLZyyk!70nD+^o=6oospGG}wK_L=1O=I>Cj~iN9Y3Y9Q znjEJoEZN@TAtm_6Y4nD|1m;5fs2NPv(~XH22Z{Rx|Ao`I(lfM7k&_qvoyZI?DSor! z^HSk`;wopUJ97Z_2MU7(p)Gpw*mL1H1ycqQFBbN$y{-K=Cpy|i)7H&BB}hDW%BK2Z6?nnZ}gm@fCnFQrLtjUwF zy27&5MW#4CEM$l{f;w&^XL+|$m;*A-ZpPRb!X=Qbd{-DmnLsvy8(kLc!wQC84<;(n%cr{yQNw46l!k#UN5U&hP<^Evxe z)HC!JoV8`aoeFXw=HIrpeC-BQcvk{rn>maWSZk5d+AyC|fe!3%%+hr&Io>MKn4uz7 zFTf+v);}LtUkl|68m};ww;=mbD*)qaGB>f5qsDi^NzS!Gs%`*L&mZwJMl+NcF(e(^ z&^TD};NJ{DSA9}E38EkUK(AaVlv(W*HUgxTu$GQNwa`OSuIUHhv-w9-lxeL>a`49O znf!AdB}|^2vJx~4WMOTP6W8!q2peVHYE7}9mFOV=nB|~=BZPmj34ED&wz)GQupp6? zA5*qlyu{0nq@>{ru}X@KN|&OYjGz@sK(DQcFkngYtNG`iipaW#PW$)ZOk;3f2*>p# zMBv(|Im_2n&2q|1fcZPzu8XR`PBV#_XN1_1rrCI`bWFV?4T;0M0#Z&xu* zKXq4qyfQyxhS%hB%x1iKK;8gd^2 z4$=z2mq6>M&&v+c{jP^X1PGU31B_dM@#VT^$5KV1uIHKPpnv9rm)oN4+MIs4g@}E7 zRqNu~?Zzzyg+$2-H7;4ayum<-gg|m0l~**DT%H;~aU>VRC)zZ6uA7jqtr(gsC3enf5u;e?XmHg2pkLhH$!I)>mM&iItuw7u7k-txE)V>4%r}L-?tRy-*I)WtYqopf)e6RI}Nox6wv&u7$`W$<~9pkx;< zqrLLJTf!aU*iV#4?V`Ty!Sm**WBl>CC_mSTaeY&G9wNpt=t*4ewU36~~Ay4g~d&8AXlWK3t7H71dmV zhb9)SS`SoRW8BE$eDgWod4GTT%28qi0kMkA9bJ34gI1ZxOArW7VtaT$=UEgN&DoHQ z-Vl*Mbe9I@u<}3Yg?ERjr-YJGmr1>sM#s`~ukGLb7o)Wm*m^Ow(hbn*Wp*k7HNs|L z^iFFd{}(eIr|@-w^a*+L6Om`ZH7#Phhx@tL9^nwM7A+u(!opd}vY|eodlRMk=^FjD+?zcwH@mZ-E?};3 zUF)d)JZ8n2OW~#|GeAXemVK~WWKmWw zvjDVUHWL0A2#3G1J0W27bovhhE$NBz9!%8Mj0bwvw0~O4}mpPqhR+s@s^cDAD6ojz-uGbX7K*TnvItJnupTW@@GmSGlaXku& z-un}sJx+!Zk8u7~l}69~dYg+2kbY7!U={$yZ0?S$dSM-~MR}t%aw0PsT@-46tP<1U zd5W%wPzdno61{MJAxR*+bNm!+kVc|NG7Kl$L^heKPcjJ(A6yA)N_Zh7AtLEMtLi=^ zu>1Fm6jVZDoGdw63gU<>6`5d}$nfX1P9s~)@uySK%u}2Jw5=44{&kwT(e|#QU)S@1 zY&Pa+A3wplS-OAZc#pKvrg2+v6#?uy!Wsmpb_T=kmJxk2S)`tkxD5;hiI!0=WFXL- zlV8V-EoV<`x`phSuBou)Qo@F1*SV&PQSQmAFQm`KWq?_U3`)Xi3ns$jx`kyqo>RT+ z(Zl=j>n;g*Dd>i|e=f3{A~Z=7fS+nlfFyxvlizLZqL#69C{C5;cAwDU*n0_eeVY)R z><_;-p(QiKwqnB`#;i7FDAAYUeeKip*7PctFdhZMHs24AFYEV38o6lSD?qn%&h~wH z)Seh@Rz;nRDCMjY98D$>nX7=>CF9|{DQl7P8VjQ*)_MWeq1u@VKl+QRvR7_(Dom+K zI?-_Pc%917Y_mVBRJW2{6)!XHTbgk0Pec0eX$)^{A*RgZ7Qh$n(lMeh!92mWEGdyR zq~@K5Vnmd;OAtsEh)+P%zZFIvqHlESU+y2>oG)CYC>FaxF#vM=?`;0b==l%f<$qhj z|9$fO*A+a=e`A*UZynbEUwU-@>-i1VzrF9jc#r>Hqw`%n{cjw|{~GU+h54UlIsWCQ z{HGHtGczm0KTfELf1gmF{+dt`o8I~kNg;sPN2wX1jrB?TIw7gihK+E1!@@VeojXusD;qz3*=W?3FJ-WpyKbHIqm2%)LTFa`!N1e@YiC}}<<7v} z@$DwV_>3vxSJrZ9#`=LG<6!bSHf5poio+xtJEQeWpFPT$Q7p`@4rAa<}+s&prz+l_9v0)cNS2(lu zj9%txa9|#i*w&dgbm0-H`YzQd-@z^dJ-OUI+p0-Ih4*tQElrwr-aJUzvO~@no;Z=A zKt`qAEaFba^)g~>dAmGi1mju*jQ4vh?;H1p^odax#Psgqi*7M^)g3)08!Vyx#U7UD zz%Z~9%uaTEh;ErGRReg4-Cay!<#AFdB1gxMm7M3S51xD)F=xo_v z^Oyh-VB1amBhf`SB#9MKV`5I*m=>H@6uSS6*iY_uSzHRD*k4#3z=xnP{t8Q&Y^pm3jiE-5z*8FylhzWz?o_qQKEWp@aR(f$+8|jU0KRKj=A^f47nBO; z_YtME*a+AY>{D^hPAvX+XrnI$41JZiT)!@iMT;VDu40tUDMyy(bxkq|n*;3ExZ4ec zA1x28w4*FEP5+7JtN^ZB%h~qEFWt6YX!%H4Yw>a{n8=rJp;?yQ*ce+7$h7_6 z4@pDUVde&{n42co0}&&y2!na_xc5 zONoNOJ2+ydrh{?qHi|OE&T-plWv9J%JHLf(8+yPY#E}s(yJeOPuFa7>!%}McKdVP;=9R-zk|2| z7J#eqEo9IlO4|Xb+@KDjy9r2-LHU)s!n6?OS|1Bg))!8a98o!lLxTB}22q$9PE&k) z`vYKTZl)SR#u^GnQu$^oU@Nwt3@TAW0L+a*G3S7`73Hg7k#~CZ!(o%v%_Q)ele+TW zOCKvaYr&Z8)OPNqHzcQ#$SWzUa+zg5N*eqdA!c8L*6(+X!43Ml&FI&I9i-su-sSTz zS8BmaNv^B>)wneMV?3RfaK%Bf5SqAhZmzt@Ovj)yD)gy#<`Wq9?axv5Vyr4H$(3~0 zQHRMKRP`q|6jNOE#%kwOo!(l^TT!>72lKLULPymP*C^*@E|^QuWz7_D z8n`!}kjVsJfDJ&$zjDVS9%g#0eeZgC;_Q&SOzyILtk+8k=ie?@1enMBTt{ zi!^V(c+>{w?xpTZ01ALbce%~PSet;ET~5b=zVKS9cu^{UtmqV}Q)IdkjuAyDfv9$j zph*S2h1?P7usvhZ1^S)|0U|%q8;(dL18+bUya~ZdJ9Bn~6SHzR@n(h#Z{P)Yi~Vdx z4VMV1=(mUo4yDq$yq!gH@4n@42ticLVzZ;-Efg_;crV3 zw3Uxl#ccY#i=1;KWR7K$tT4WeG@n5Dj_R4?=^Ui@Ouu*;Y&)m~-iWcRbBSd3Y zGr0@$v+99A0Z?*;j05)z7+}q-C1pC#-A9>1-+->oegam_!xc;xy*mh<6`n?tRAACl zlqh&r0lEeV&MOx92{h=Pr)}R&0Jbzy03EJ#tBX^L_4_~rT!}d?U*Nc?s@%H|qi-`$ z&1x{?oAqkV2IHUDzeLhY&0ts(U;};gJEEkEpYe{Km6u1{U-2>kA~u zi~$ZN637@kez8}*6E5gRamR{;v#wSn4-_lecIpvwazj&!iXCh&4kr=tW?IW3s3j^+ zXOSVWih)0iyaHE2I6+N7cO>X^O~96^rg7KDnxSDrPis6_SMGgN#t)SJB4XMa3+2~7 z#CTHaoL=8AD(z7Qe!dL2yy8%Kt}QFed%tx&G4o29fGLQoVjv4NQOkV=SQK0k9LK)i zA&DNU#p4oUtSvv)sh9q)duNTKz&5Wsjq}Zr3ad%OABau0)`0J+PgM9_j^t%OaPzQ6 z#8BT04>biVl(vSdOK+GEMnq0Mu@8KhQ{iiV-f++r1*RZe3=JXK;j=tZ!SQ$h074udQYSqBS zd)m(&>-6Gdthtk*RZN)1Ui%YP5#Du)nf@sxRAdYLAsnK}CFZKbg(pNDz3?jF`Q(z{ z>cHcBG@F$qtDY&fbH-?YQN7&nWZ$ywm()-OMg=-WvY$7K3hpHA>)nE$;}?+oacqI%qjx)r7rp;P2$A^;w=2|TrD8{_P1AIHt7EOfRy**TEfHgG+z%Fv2JJjAbjZsO3k zX*|QEN%$Xst<)kE)I!<$s|7vg`3Q8QF*WiJC@^_^+_~jXp0xMgy>WjfW3kMA2Ia|Ls=E`>tPLPaMdr?41e&Yq9(wuc%c5nj{a<=3( zE>z4a7U({FDl=raa5A&)^J!Ng<89CHR5A{q@c|NocuOFfmJj~y{UQI8WXTz<&gSZ+ zwavQn`xOv8SvSOhrUJ2&M7J?gQONFg*0azIlsSm=ozq7X#E#~RgARLjHx#4UNPmo$ zmEqwlQBB5iofV<;NzKk9-;@OAMj*mzE;Bvta7m~b{e?)%<-AVD5~Zx{%lio!Iu-?T z99@!#Qoru)@xt?YDK%I?g#{Q3PZZ;EL}m<(g$R!vcu^l;-Cy<`0Y5!Rf>FdFH2?n4 ziC#FGJJbFc>)fTrH0<(8dPXxU6o&|E_NOBKUX#>m*23PoD)dG&j< z9%k_j6mpksT;@8HF8(psgm_DKj9Vuk?af}37CpA*z(*#*(B^0`|F1SQlllptNq-8c z<0x{YG5MMEJ23)#@XQ6pQuurC(B*`IEIVZn_9Jqws0U5ai^R(``O}U2jzbhGYa?YH zslYIPS3$fWfR64vC7i3Rmr#Y&=E@On+m6_<7+~8qCf-%X4fC3Y6<}F7fX?*TMk4{& zj4$r1Sd4HvfRF~`ZDcGZbkA+_yzwsEQd;Y3 zRnMA|{;2;F1wk>T&rDrj|8Rc3qhZ*BlVx!smvmp{_VJ_d)2qfU)5~y-4zfJm6XUU5| z8i*sz@+`QJ{4slmM?7F9@pG{p@d!dc?jzt<7L{ zSn#Usqco_!O2i+K@j5(tTW$3u^Ew)}Q_8jNmmxr3$fi42 z#4!9I4(Q9FHm6mfJN}dY`RA9BUWV>Zv~BzvEi}t z8rUD$V@Xf*1Tr)hn<0o{H^=4F3@0M+rjAQ6@ypGoXWGbOkY}@``)1ZkY}sC8IptxiVP8PF#G-2niKtVyXS+5sglrV= zt1uz(72Gh|Gb;s7?-YCpk?`xhJCH3+$u7KtzuGhLE|Yyt6!>f6h)$#+3R|jRdCJ@f zA+*XsaA|p6Tj!sC76u|>EA+JYNXda4c9c}(T5-!C+~seKI7u9HWuUZ)mY%#BnwSMW z19Fsc(R0=P0FtfCmbM^(Fv_w3M0sY0F?aDgyo>(WLpQWA9H?UR!dwN#q+iwqV~DYx z;?_h77=l-Ud*!L|XI*@7ZNmW+Rj?Tp%aS=Y zu5O2F3A^XtFQmk{pDU;meE*CTGy@Zxf3>OqstR-QX3afMXSC>@@*oKYDLWcEz?6MF ze3M;WU@_yU1GqcF$z4WOvsKEXke{~#cEGNxDkKa_zki8)Qz%tZi4UEE5`Phl?9%n~ z<}=F|BcoYl1;p6k=X66@mJwo~MimuLEMmqxYv@<3?EoK-sQ@JMi-9ERHJqMXaTJ5_ zD=GxIo`)6$%cirqR5P?3SLe%O!W=f7hc75l?l~@PS2w=gn|gubiHFa^faLyEhY~Y* z=Dy)99KOLF2<4BHhZzVZ`;bY;l-Qq3q$gv{mNthF47^T8_D>IQKfj>q0S0036aR_U z^$%So|GBl}KZCrn{Vn3c{2g(DIt&RxSnK(B$XlW__&+=UAL>8;-RAK(Z-&r+N518M zQe687o5#NkYApXMwEf3fmw#H0!tx&nkp9*3!OHl*)l&aOa$9Fd`WKShLcZxUk%Hnn zO|?Y(3hhk$`BDp4PdmQ0R=Bq~b&Xy+Sm@go8xsntWO8DoDL?uPB}$i3o?dbXdmTRL^42i6Yu4-)kU{t`oQ;}`petO?R} ztCPY|lr$MMt0-C_lX&X14$#0wR{QZq_G6|`2^?RZiPuCCFT8W_2nSxsMI9TWl8(i~ z#w~`k_zikwNXrQ=C|^2O*pr_u62drp`3;NT>KCBvJTn{?)Uae8@?OYp08+C39F&mEKiZ@>IianOr;OTgcN9 zuafOsEDqWdl=Z#hWJD4Szbtm`x+981Qn+$U_G99b0h;~i=|MvPkK=1%Ml>GVWl`o8 zP3gAFww<=0tqNUU0K`UcN&7`0QRFr!&~0Zz#WA~z4v|D5!9aFhTPS!|ewz1yWjy#m zO_OAzFNa#qmu&4rv#JJQK#Y<|3I6FdMzp$@FXAzC(rjvL6)1rm;*Ns=FoCe6-HX)9 z^-?$pxKk?gkMeP?vM3-kV0W~$K5P&Q!R0r|cs5cCFk6vu2B-{aUIIJwFPPE5lsRfsAq`cKnV4GduHn6a)IeC@(~|h((#r3u-<@NtD(V)QjQ? zkaFpEIOW+X99&zfyg_-`LKieJ2nkLyH~{MHm)`=K9oKm>>}CLora&f4YZ0SCr&GUX zea|mPSD+e)spy!vvFB)B-RRbPtPT#$k0JQ7bGf{k=2}~>{U1s88{;4|lOo zl;dgtd&1x1l(sH@%6$SFPD~cm4m{R1dKqT@kETMQm`Wz`!==R|9hghoO$B~P67en+ z@*kcYT z^75>uUox%cfqX~94?yAW#@Kx+S--IZ-sKRMQ2p_H?k4R!1zMVa$?LUeUnUdIz)Exf zjP((HAp6CnV{}e*1NwkISPFogxkS~*E2N0K9{!A!aB~{IAK-+kF9p}yoKz4_LItZ~qObMd z*`u`_;UW3E2CYM&Px(+;hdF5hbWNV-R1h(j92lS_UPj)tPLgz^21hk=O!%EHWdwU= zTIFEKPNuaXEDy9I03?8RJHf-!GTH;JHMmL*>>(b{B< z+=L3{$#V58R2p`9?^Jpc#Izi-DX|Au?xCVcwFCWAMjbYy9H394M0#bggwC|-r*@P%rHSZj z***e^N(>evGnLP-=^Wi^;vzNJ%SFtSZNtFE4SKK~<#hJ>R+f3sLZP#zr%4(N>yD-H zaOwU=)Do&uPZYNi!-$ujf=i;pa&o#ipv`Wb&Gx7s)AYW1p} zFdx|BTx1o@7!a#{_~=vEXB%Ol4f=sr|@b<#_;L8Uu`r69g>aK z8rj>xpchamkk&!JV;5+COX1jO?}%~aIvOx)zL7Ngpn0`0Y^45zr8zt$wGn{)DGtq?Vb7Bf+K5wx z_f3Hd+mI>2Lj>0J{=!U=TzU2#@!%1uPI@@jZOJ8t=7bZg1lZ&*0YfSUKKXXtQL&`;%QQ+P?$Q49{uPo${h<(dNXvm>nE9F{Iw(hlvd(= zGKq}H1YD-q_k*nkeC5paWAzVnG8k^n@(1|3EUj6>m&yc4gBCbYUAcTUJMy`Is9Of(=wbrbjy! zR@*fy0DIc|ZIHR8MiAIfv6K|OopBP(zlWvi8Mn)cb$U=WK@{!Hm5gT65Q9>0!qN-| z48swL20qbbm`0}3skHl^V(XbY5X_?=E^7U=9X$Ep=^f>CT7&)BVYpJ9>UzQZB_&1b zK2LhKm(U$Mz6Rayk}|z%+nmWyOy4;c;Yv#)Qpm+)tCP?3sugw2o}p2(tm(1PIFeH2 zs|YcBY7Yu~r{o6neDJ&ir(sNl!ky#yYBfKMe|X{bEz zy38H2#m>l?!vcn)Av zYRC|T?CAv;;5q4QIC1M*$+Fbs;om)&1?;D zR3rRE&dJ!CWCa#+nMO6tG8`2$t6b#x)^KEE2NA&m76Y1M!Sf@zvvs?R7ru#sC5ZDJ z$l`F?vJ5ILSBT>+izUNS?Kjwou3plIf2{G8CbE_R9WgqCg))VmVKAC3PTde>)X$4j z`9@8#8%O$Y+H4Z01_yz)0hC})Ctg*Ro&KHz;c$T98HpHU-`=Mia@voX-)Ufn7Zqc_ z9Njw}9XWVu_89}8G&#h6YhNNh@q*9qIDpdX!6y6b>2$J6PA>88+pzy+fb*~fZ9yvW z;#-e781_th+(n)%JwA5Z8Tyr7D+DWJ>b3JKvG66jFg=40&-evy!@&K*e)OP1h5Lu0 zThcAG1ho|8?UzW1#g65&_fX>q$6d<@G;ikB@c zhbBEdcOgwGvyYSSr{DlhyBbbd0NeyZMRQ~!iwd!YwNBKf{f_dB@d{GYAk?%^2siqv zTw^7vvNsN_B`v3`=;;`NXD7;G1#Y3}_Om~SAf*AUTD#kf73k9d-s>{I4w==fNg08t z(7D7J$)T1vNOjMM&w`eqH%TIvq_Rjll?ETm4BLm(-g{b%-8_mYlfksOdTy&b>P@$2 z7_aphV;7J!5=NQc2u6Mzzl2O3PX7{D>h)AK)%WY0=uMmDuVsoHeI`w-HIp_);2kX- zf9WUIY~5S%CkH8IiXbB&Giw6@Y3Dw^`TY=dbhs$rIj_r_nD481RQwR+ns_Krr_hKO zTcFzno8SKn>i$rqRv?WBY`g=2wZ%c&lqJIPxiylgi8;UEH5m@ds-$7Ys#?s326w|< zc?Do^xg|DsSv^UMC@=|7=YvHU~-#J>;Z|BZzc|C0mxzqwle54X$T2KE1N zAZMljrw*6@TKNP$GuuBpTn4_&C-y}DRz6`$Z`nYHCxDvU=;Els}Cx+tY(w0*T*!J?8m55l!l> zC<-V$soJ@Zqy5L`pxhOqGcB>jYq4$m>luYo6DriFV7cUboC%-Om%{R-Qd*lf6}Uxe zC|3S=|Af$Y|Ae&E0O#O!Vh2imZo;xku=3RSbkv&u>Tt$$+sZ}ycAFEyS^U_iQQGN%OS%@qVSNb zO{bT(mw@LP7pe&QEey>1=Jz6b9ZnwB2G&^daPB_#pGwe!6d?3fAJyLBK8oM5f`{ah zKaY8nwC30IIhaVM*?X=6f0s6Xh*ts+lQITl&%hJ)u2LZ1Mawl}&{d76B%% ztx61L%gtHD#Sec2k+zc23{|4!Uwju(03+vuL#?&~)JIAJI3Alty?lLM&|UEdOshQW zzFjG_!BfZfJe*)vc~^EJO2^`m13360b>e?cKep=uM z;&TCP5GZy|v;Z3S!Ix&6asq*}_@fLfc`NT(RdH0+2UqF=(Z+(PjXzig))LA(wKHWB z`KCs-1Qq%Azo>i1=-B#3PcXJ^+qP{xIk8WCV%xUu-tNA$ zx@WDPZ}qA6TK}rryMDfeD3zW}9}?k0PlOo5-rRld0W3Id+}L=>5Cyw0cEr*A39@8w z{F*jwMd>NoMXZkvA_i7{J&D;-k_zDD!#}D+Jc^tLQAfB_@R;t`QP7IDLX8T+>c-$2 z2DWJv3Jo9Y1dE^-ltP>gv!-lwY2dIzK+h)8(Y+&3uDx8ZZM&0zda<>=1asBifMrw` zDrW)gt#CypK4z+>?G?2Y{yVOied{`0?>C#F&bgX?#Bstc3TKh#U}drSSGFPm!ruzp~$;JP{@Fuw^XJK!?a_P8U8D zUE1S&xAlVM=?Dya+5*{%>T%JHDa)DE0#c%^m`S+owCjas!YStqkR$+Flro5~0-!cU zdE3j6o_4H}P^Gki}r7|}@C!G6stg9AcjLLhy9|_25^#@C%Pd7dt3R@Oc ziGv(rf;*_w{EMa1dnqSb6n=NQYWIq;!3o}bHg0$e|fC zETLhvgd|pJlDog<*14lptg%Nq5|nUuiQ*QB1ZDmtua`PH1gUtrC7pLFI!x)B?X^=C zi+Uv`d@3Y0AGP8@XMpQBAF1g)Uzh$)X=;{YO%gWNM9V{4L2aGQq11y6LW0yn5Jhp7 zM}o5x{*wg-`D6?~5HG=!s_m}NQ@PsNhsNd-L`EHF7<8KxV4gp?aR=86i1H@)ugu_x zOUuY7G9g!=0+@vsG4bmb!e~MVtN2)YifBIsgHE$$3nS(?0Sr%Mx@oP{_tNjWd!>5= zsPCn@%;K~6CSavBxyJ-SLrW0}gfZbI#UKx) z*OaaSPt46sqxrM~VI^335TqK6Hw?VO)p*OG34|_nTVvmH%<4~E%OH~q3nj(NB!WW< z(rJ0Xk$+ahr$eBXO_gghc@9RN6!EO#_Q;Zi1-MVMaB%?T z4FumvP1ugKr#3n0Z=f5CA(0Rbd_JFfR?$VRo#AxTb<`iVUKW<^_ColKXg)!mdz~dj z+*gZ|$2qdc2O>K|8(CNPUACn&!0SUIir9XaQ*yFUt?ZQDW*v%o1$zd3CA5e}d+q}C z?0=jv$|y!C(4P`();t0^i}rv_x6*WZ3XKw?PB2oXMORYMjEs5SOl)_R$xAu%lVomR zGU4i4RI#-m;*G|T<)}(zDp@``nv0*AWh!B_MP$T&>q`wVqj+XthUL=~xLc#%h{oV& zlV@xv5;Bfy>MXB~11#+bFdq{xYUzeT`*YgqH~i7yUDD1}%kgHm%$X9^T+)DHMH^5@ zWt<^Yv}x`4-sKqO&BkFCNBP7FkY^XBrL8mld5!!boxRjG*+9S8Rp)x=4tIN(uKvjG zRHb3L+;E{7{UrPg;^}OJFs{j)nC<$A#{I%wX(^^R_)Az`%lm1mF+Yu|ZC4s)@29b& zKzbPK^*ErY)6DUM)7H$BW4IGR51<`PMfDFxus;by!1u%*mKzZ%UUr0?1IX18aJbI2 z2=Upub=JhgIF-ew8ioeNFlF~HN(pD_9gG${KpmC4T;TF3XF%BY6;D(150JE`K-KE_ z5hZ#1Nl*N-LJQnQOIFF+gwk-u$Nn-Rh`#9wUqrbRq$k_ zD6K;UJ5kPd+qd3rn;C+sP>_=odDOV%o|a|7_Wr%|@+euCSRaq&M!S^u))@9#tTpXUge zSUCO}Wu2>Q#(c}3-xYrZ4D|E)6NiMS5dHuyUCRBcg`Ie*(-A;f2O961{{L6Cs+_O@2iXsQVlaHlMR}80~_1aCg>rt=uGK&lcjhk8bBTDn@V4Jf_INyB ziBP9s*&ncSc)RT|9z*^P6ePcmTv(B%8P-31>z>GL+I5thm~3x<1qx~uS~GO&SmajD z=Zq)6P&IW$82QE0bMiB73ldY_BrGNqLt)fuUcDZt!bCifWN+ZV0|n<>$ke+SQIl%O zlh=Tk#TK~h6gt#FO@p*)cP#0HnH^EZ;q=inDt8CPMt|v^RRgaiBT14`-ru^Xv*TPa z;;3U(xjwVg8yVFj+m%%4;CGq;LK@6Kvbj4}Ar#3Q|2s|K`Rj>iHhin&FWobt^w1d~ z!GVF02aoC?cQi2a1a?E?x27eM0dhZ?=cb68Wt4=Uurg`G_}LHLDKY`NXM_<{Ga9zq zVWv>E@|D`u$}dK@4>a>%+yb-c2QNC_r`Piuc{}*cpn#*8fr`UCwsrSQbv*q2-@<2G zeG{iSz_;ARg*k}@m!}ea)65rG;3dyPeM)_i{d2!LUdcEhuA#RWV{o2pT~AE8Zwz=^ z`PTMt;nOM=s1Lw#gxzeP<}LT{hyhp?R2}fr7GdKUeXI~&5)3-20*{m4X0xa*k;!wm zO-hXv0|CNTo?>f_%W*fFZJ#&PFQnUn4&k-?Ion%I+rMY|z<>eI z3+L2e+Fm%RVVeEY{ORFJ?R1<;KYL}GhHwS7G)q!w6X3cN_JFeM$Ab)Y{epa24Thwk z@p9+EdbHbp9BBx28yzrJ@>L1b3K*I%d zWkx(`V6UOfe7FpJ;IFD^v2OZc)u=OQu1+LH6TYo2ciVw2-Cq=9r??jqhrbx$7hNX# zVaw3|SQ^Url7+<(cSHZADlN6#0F6u>l8~f9xu@MAgh@kGTY}tb`q4Gtci6}tBTKX& zmd|st#;Ce^yfG7^{bE-EH8w5`M8hwz%xaKoFL52{?1jD}LUO~pxwvPY1x&^=PKs6zM30x@PEN{nV7)Ov&a#{}Hc%JRw+KTZpPG9ye zpO@3bBKaoZZHot=L+-urwmYA5gqjuN?@fdm>_Rf!1fE-XFx{P_vyh|%|8{0A{2Tjz zspofg791Bemog0W`{t z>e5oSqY@E27l)Svlz`q5YUNu``zzg)2lA(#<7jDjIw&i|X)+moZQ_mcMs3p7GL^la zxBoyaI~S-RXuBc4N4mJ>T3%Zw)v}ru^hmuKiZp_8a6KyDS7b~|T)wE4-mI{5a7YFy zCph*DE|$Y&;|f;j!1u3tYfMT3h04BC$EK;K4~`TAPJSSJ=Zd%4kVjMf{)lpL}E>7Pv20gx?t%H`CClw``p4~y6F@k{~|7Dv7v0!<|lgypC#eWR>&ZsBn) z82#w41nbE2*$8dn4936X-ps$*+b(j`q-RCqg^*OF?nBLqL_V!$9(ZIpHE3f|r6Us> z5bOo}Seh&TOgwb&24;_mrfQW`y2B;0R=iIa|1Kr_1c<2vX+*p!&2JT>;wX=q)p@#E z|An*e$d^-o_r4=tI@OyaD~$`CQ2}0a)8NQ0q^bH_um;g_1%g!FK|_*En3$j^Zg5j< z3hY6emC2|$Cb67tX7@|#VM?{UL*-6W5JgXgQ+EVzHYkw-TdKn-N%ZJ612xH`yj+Di z^YZ@1quiMyrjxTdvqE~zck?6}O|w3T`osLg;`hDEFDWEPvl&$SZhCqd%e~dUGFfv# zF2bt8*uuu}D}7cMVIm64R413VDy*_@yHQwrJ@_`LFq1-|K$j{B>d4`F^+L6xDiVx) zt`$0}i|v=YUnarG9N51R$dD!3R0vN~sPHxkp0GjYi}z|yKx`rZ91TD44pJSuOcf+< zb+jSn)LIqARv!6s1}!MEh@_WFfqZFj<+trOi*xNs4;|wNU}*Yq;y63CU7)~{!|Lz; zL}N*Wf?KX6H2e%Fy)YX~pLMdUM$t-8l1GJ%+#TiXjO;c)c?op_$y0Y6TBnVw1Z~5z zZ!020C2O=|(C%Z?A?;f!+&Ax(qTO|+&^t?^!d($;)M^!nClC4r7-*?HpM1fN97xxC zEul4m`qA|EX0xg0*L<=)GtneU3L8hPoexEFHE%*oqVA>&=a9qk%R8oQLBc|Yqj0#m zonzpSxeSBcg_4DCAYNmYCD88@L%|se9vySV?5jhf~%;qTxG8@!)`661hVf05Wd$(x_-5sMEc zEYt0pBQEe|+T_U;Zu@0!u%ZI#Dk|-rw%EPl0p>gG-*=Q4)X_3DsWkR1PuV1udU)S)?47^ts?^{{?x} z4-_8XbAy~H_yTJ41e4L@?OItYbWn^b?~>^<(8_`4u}p(Q9clOLvm%3prmG7_kyhbl zrXj4rk*4q_X<=S%3m@%mReyC`5Hzo#>fCaN4T?}lC+1j^m4X7%Bncu3rbnl1p4eUK zKp0ye&sb*2K_B=_Y|Y@zy{60mZMAzt0gX-fG!Cpv`$9G7(K5umTo6`D{EOYgpRz{6 zIcWwVtieVfkHkDet~f=u*2`CR^}eP|$$~v9t~voyt8g|rd_$?H7^z2$ik6kget~NtIDFTs zsVBWr+|^f!xqBMi*}*ll12Z97=1xi0<>L4eVWC$Rh{J(s1FHjmz>qi4=g7{80H!Q* zK}8PF=Ki>$&sT`olg*V2E`$HK5)>b+ikODEBsfB=TVdSCoHts#;m(&r*5{?r1w72M>_knR2&G3Y$H9Ov#Vf`5(-tY|!(3Qqm0gNaSinr7 ziGqR1Q!be?^qcfMhx(94M+11nWU)~oVv|i&o;|0x(5w6hm~H7Cepdk#9C~$~gBWPd zL4S+gXti5cP)Uqw(QcvWAJ2qr+zK$2H@wldT#oNM4YJ z!u9e_Zj~kn#GfO+)vhg63wF9F&q8jN#=#k2Ywvu^4a=gI`#NCjdsj^$62K-j>I)`S z5U8dwjfEW7ONxF*#uZVBkkJ~2R}--1=QD72g4B)nU@cPdM=_s}*J57EA&z1($T6j4 zF`O%38w7KJpcNdsOu6p3$_b_FHKZe`$MP!V^Yr*beTFjn`H$;w@ONlJo{bBPK80@# z0F+;af`uLsAv=~&B+|=;bRA+|fIoEuml@PCuUeE8z`Qj)JL_6r7Q}h(Vg=Tn`DymY z2%l;W89)DyfgKa}*QrHnWw`zm7Sn)&dlYy^NbDpKgsOzUN+O5?E*VH#iKuD^1V0HQ zB@iCpggc}x62b9{E;(H!PTO(-9T8BdL{HPvz)%Rg}Qv+P3ubk{?2}e`?+O12bw+(dvkUCk*Z=e46IFXAzpAHQfY&_ zbID?WcEa6bxhEDuhP@1)32VZ5qy}j{L0k^xA2<3n5-FAp1;%FJNA}JQ_Rd`2Mg`-0 z&8dJD5dCBvuBhxVHUd^CWi<+Kil7&%$u=ox6PiRrfS=pV4hgg9>Ij7srpSj!MRElB z6T^}YGG{?#VR#zV84H%goyc3GS24%pKY1Y+)(W)1b;TBk1mI?Kz$y5!0H89^Z~@rH z*?I5Cvc&orof@Q@YZXh*|eBj-G6a36V~J*;v1I z38G}E`#0qJV|zPZ9tXDniOK=n_=}&1K=d$F1qdJO6aoC)_N-Hq%XdUz%JbBouD301(tiVjSs8r5BiGdczyP8J1RDQOT8QnhDvAF-yYYW-Q{<$V zBlrhPB5!D^>}dU;oQL@)6C1nI->VWVz<*or4+*%?2)Y4S$3cq!#fAT=*Wy2<{eKif z{Qu5t|Gy&b|I6}zwz3S|3x}|OZz!`GXi+CIvTuQ*DUQ|o}l&s z!~q(^5oGWtUNmoM~^flul%OY^ly!bY7bDUY*K3nK2 z+^4FJ?$3(b^=!xXT370ps<%3QhN?Cvzu(R)*~(2nzP8W7!-+RXx_H^si8LsYGJ}2v zrWz)LzfK5+k|yynKb9tfneEG=al1<#vbrGkig*HEv1dtfFvGf##Y{>hm>TVa@#{b; zpEs=d`S+p)%h!@B(O-;H1@+{0FbWsrrI@KLi9?N$6fV!4dDY#lzfZVnc*>i3@yo42 zO*L9sc*x+YQ8BR*7Cd1R*6PVBgAgV&L%IJ1%unjl=#1mWT2k>U!^q0r!cARU7RSx~Cc=J^`F7y*DQ zDP<+I(BWS+(=Q0A;+4)$t&hFaK62m52N0PpwjrZC6JM1C2v{Gya;Pqq8PQXD*75O@+!#usr|6&pwZJqf_@*eu}r>;7`jqh`AX z>I(ab@3N{LJA8ebPwNp1iYbU5DsgZf;+J7XejQa=)z-Z7MLQn5xK zp4zHidAzB5AH0bVz`640JrX}H#KEgaLl!m4Q>hIAL_xRxUpgh{T_V@E=VhjbWs_T?YP?(0{w-e_Y_znu_WQ zyiIeRk-7EQw$v-Cn*Vn8*!hZpJ;AMUiOJwy7c(YBEt3psRM}hYlcDCy_ZdVr&1*qLtE zgM&kh5Ryla|1*(b;ggWv1#HT@SFN^OFtWqR^(=t!eQHbCapq4@qoC%r8%x^~lAgi* z;2C~}C+%z_<0}56H>iI1=73NaYspK1?ys{{AsF_jHnqe`8Na$fL>9s0SJMe9uQ@Z} zDjJ-FJHEV~py3~uYk zQF*(cv0|Sm4aF=CT-qcF3PsD4ZRjd`Yk;yf=!V7NEPik$Zx*hgX440Ep9lGo-%h7k z7Ss>b6o^_wzL+3o=MsHOFGk3zuf2k)Pd9Cys@EydBn7et$Cx7Qz(@4)8NOs?`zP@7 z4_Pj*q%RQ@{Sj?ppPYGun>18_R_v{Fh@u4pybn^4P(B4S%IR!{X=op?*i8kwTR3F} z124vH5;P~GS;oD0r`^qCcF(;pH|tJbA6QZRM|?<7fNwo zk9RFA2%I#Se<*80?pn}~^yea&7)OCe`B>T?g=PNRR>ppG)Ci;KeOqElM=5>qePJnx zE{-bL21r6v9G!ehlUJsNWa19E|*g_LZswuW4*-d^LUXD#vj{Y85svojBP8!xCx#P z6mpyVw0xtbCn1`23PmX{tElqNEGaHtA=rKkoSZTMY@Ee`jr__fM*n!pCAJQhX(D;` z_wTFT7&b-|Yaf)NkE_V_ZgIyb}}T zogImH39ldN!AwsiYXR+Z@3V(pQ&QuTvk1q8rt8`e+}Z^&1}#r+QTgqbM)VAV+`-mh|94XsXO z-OBLC55`2BS;^68eLi_*{#>p|6?QEYN{nDfrrS2X$RBw^z#tRI850H$EFe7?#j@a_ z=+k}?W~XXlz2;pT@OE_i23BofWg#*TPi7N(gtGv(aT}UQXeKkB^Lnb)5~g>)21m!pd%K2<*YajWb!5pD7vu-UyC=>Y2*8;i z__=?YGKY=^EnQk+2#y(hdhsYlQ!%>oe*x zcRj;lMriv#C)Z9qgY!JFmrEZXQ*K$uV(P+~DaDOhF+u(2t*W!2PiiMQ+GBA-KJxbw zbCGNNu{U4v$h_+&=N_qa$+{l^NBVpP#5gPm?1KCMhaxb4}PM{fiK|aE{*PyH}jJ2l@y7`a@+oM89k0U{Y>Z zI+wDbrW6Q7CMJ|u6$u;^y!?Q6FgiwwVG#oYuV=A48h}zFX#fluE?CLOmRy`d<@xOi zv2{?VNa(af@#L#ON~eMVTHcw~hV>+t zWQkVM`)8eZ-sI|8%2<>s+#HOUtp*TMR)Go0Ppsv(VlNydy=7h@NRXm3;0jS#Lm-K?I;)YDw_h_}R^bq!UZ+f)#Q>alhI*sb(;jn2up_q{y?( zj@c?5i9YV=q9+7VPQvfv8e#lbQPOd{R8p)RsiVuO8yi()e7)y4t6DkQZf^BOFw@vv z$qr$}X)9J`r9Lj))wJvBjAg;`JT|Ks$8jL@0c)+T%Es!FwR8GCp|*z7_i6RyqJT-R zcX`z=Sg93+7V%%w4-xO#Gnez;pP$_5PV^2mbg!=n=d%*1IciwHT^sN*3w&Kekh-?r zZkd3HCeJwuXsQ7EJNDSg@i!4+(P+$DiJ$t5c{BZM!)ihc@W`j`J8KQ91}{0BLcYah)a$LRX6%%#$B$U#(;m^4{BJf1?Mqy zWRJd>U^bfDhIuC*6D{P2u=9x#R5#xkE2J=~-P93D&=9<>CpB3WjO}D9#fPLJvwo2} zCBk2ZJVRR+ZBB4WMm=X*uGxO{vxc71ma|Giu9R98WZ7v(6nu6{O^t3@y^Y*-)!ee$ zZr_e*ZtT^7!xQCcq~$66Wkt^e3h)2X1K_=yZEvQ0$mG?Ng=tbWLpMS7 zb6qe~SOSc2g@mNZ8ukjtymi_1RGKjfy;w%eaJW^8wQKdrc!-S^I{_kVj_RHZRW*kx zQ+Tgj@+v~yOX($&r8Yb)6@&mIt|~y#3-hkwsI41LT>uk+|AMCrF8>J*wQrN_gqd0z z6*Y-t$q0z~mtlqwXA5R}2vUWPpXXIaOCb0cenEyWk{@*SP z`u{Be|1U#}68-;5Q>8GI9s>c}KU^>W_1en+4%%mD`NuToUw#v||B9=h^^g7IJ)TvWTy5!jz79{)9hx2C`@~zd7{%9a)bVoXOpz*EdH*5{N(B9j0Lb}^0MMET zwMx9qORgN3X8Uq>9`Ko@v8~J8GE2XzORuhvcd;)1CIF0ssgpmLzTVRM??~_lx-5Sf zf3Hkx#vQpHdpZqbc6P6^Tm*mzc@#}AHq)xNkDGFu>D8s}M!Z>l<9{2w2E<~PK4kc8 z6mfCO24yN%4)GE*jJ)w{1=J~;jgFwvzO)ovkv&FcuwErW4JbMW5|sS!YP9lg7+ zW38Jpd~lbPUKFYhxY1S$Hrq*?Pxr?Ubl-^=5DEI$kIpx??y_2;J|BHFkWy0w zF~T!0(UYxKn=c%xqhA-E7e>1Ko~2nptxMu4zs$Kj>chSy_68LAI}<&|LzAt5cd6rs<#fbk39uCq*?|Twc|Vl z2l-dn-WGK^uNTzU&Oms@XxMt@2Iz+VyhraJ@j$gc*2X>Aycg>NV|GObT%OxbyxxSNL%_Q}Zgw-> z8Re^i+GV~Wz0bqHB>uZb{(h3!*6VxJs(VgXF$C+pb|uf13y zY+?Sj(iPL{^!**G|I`J`!$EZ+AF;3yf1LXMZnT!0XMV9aEDe}OAldar_mR$g$6&jy zEk_3W+~aC?2e2mIas>-cIn{a4$S&ssWYSx`VE_pe9S2A4LqE3yg?HTm{U!50Xao!J z!UcL9nfCzgPYuCV|82r+jB;LJGAhW3!VhkFF$U`2*Q9(qX0#tIvV1)y7mHn&q5apU zSN_}3+eKqs5 zQozJFQ)o-cpALrBM@*{+k=;VU6Wt+nxzA{!*j+8c^3(OG(CRQFpb{LTIzqj|H=;!V z%BIH97{HC8nilH%p?u|97?}Am@r?A23L$}Hn>$XEeP|6RS7a*hpdnK3yzzk;TIN(n zNV*!(O_N5tnL{WI&Q}5F#(|!#6s=DbY0Ki!}rsZjdFWaMhaZ13&%I0ARE=Z+K zKe1~r!IrQ}sv+YqFAQ@Ai)ER;`&{{mkhvXBa+d@a9{H1`@cKnNj&ETgHp(QHRDld^ z=2S;>XIOsx5zdA6@@YTwJA3yA0omvCMf^ja6VRW>Y>^;hC+(_`rs8Q*5gTXXw=6F& zXCFmgfp6xqxLAUHxm+a0&o_65AjJ#SZ9azX>0VYRe*rOliZvlyd+H#_cE%TnJ z6_k(c!Kok9mIr<9lVsTFG4v_c>(FK^UhtGPb8x^qv42BT8!HwSu{gL%Ed;s?J)1ID zR;rwpE5uAyoJ&LWrA*yNX(bNmCtH-A&2OhMjsmNYjYZ2*3XcQ#q1ddkNaY&W9h09N}8L1ZvAEkSxArC)sOtQoxJ?7F0>= z)E5F8bflp}dVgc=9->(c?Z835}}x>M^Ujfb^o2JArPvV&IcImaZ$6hf!+faT88^6RvtT zJLjpDQfic63*)gSFWn3C%CW^B&$HH0bhsh+ay-K7@=4HT}sfE>Uy&`k(XZToeV~Xx?4U*!cHPSVlL#iLUrC<_&=AVcojeUCr8M7KIp|9|uMKoheeKGTk-!4?t5(s@-B5gq;aQq>_lzKv%9QfI(4YUQ&Fnw& z5dYXw`1f4}wtovT{2zi=|H}aNKerTE{vC&YXCPSrYZ(Z(e~C%|I*9+N9L>be%=Djr z0Ew{i|EPWwbo2vOorl>X&FcV?Z5?oi=Pit~f9v^yqoXO=o}y88!E*8Ont^#56t6ey z482}lDyq28Y&!)LLQcQ9HgHh6TxT{Vr)%}6ez!HV{$!+3t9DCXPQGHwD&3@)A{mR8 zsV|eQ_lU;n@8Y+afsK%h6x9(qZ%VT}hY2~dduP5jEgG{m(MIE`oSaS`qk_4SSB^qYOh%t~41EWh)2Hcp<4NGEnTj;hGWNG0{tdJeUBTy7^)&2k!J+AJXWQu!A#^ zJK6X<(fC`-G>Y#*g|MA)(k^*#T*>|fh=Hx27YQTy#N?Ht(<7e!m~s+%S!Lw23_JV! zL5ZykGQ>I!7Nc7vMgwMN#+7dNys;HQ>dp|0X(uk-ELh)C^lm?f7=;m7Zux+WeaFeZ z+>0A%RjYg?V2;T)^AzZ7#QV{UYCezI^TE4lJ}SQtdu$feo+=HTVW|)dqq8iIYN!!e zVG%t`cP=J8-%xYV2LDwo!0`dkw-9YQjzwJulNoQ>L?@mSBhKFic$$fmdt%YJq4o#3 z7>k_comEBkS;1|5UR|}(E3_;j436x<&78ne3Uh{?ayi#Hy6JND8X+45$9v-OlpXjw@Qxhr@PpH1)%i_9kOpsuAfWb()SwTf;tsLvNnY73=A^s84 zaAhf~0KR0(f(iDdpw-|atE8%*dXOU6+l)FRr6BN+A%(v3d670Uuw}R;0N&WLNziUO zDZu?{&e^Ytm9rE81?5I!Xp1~7S`-VyxMUGREK1n0U}0QOC%+wP>87A=ofMSTTV}Ce z&JiXCPQZ%PD?pueF6I8r9iwQo_dnb;tHw+eo5IV*6mwmb#f#rn^I2nuZYsEq#nOC@ zWr?0S<~gozu{NEr(JNN!L>FP1=9x7R=Z~2Nhl+(DR!ITNe-&BSJ-Uq@X=a}CK3`_= zMcTmUX=gye_b>Dw0|Vv99M*ui$oORZti*o1DTsMw=(N3#(;r;``PTmYS73y274+!e zT1Z8U>#7Z8v1Zml+=(qpcX3Ffg0Qyn2xOZw*aZ_P=R0Wi^^j@udXcb;)HVy%@T4TD z)+~KZNjoY|^{5jc^@|Sw@>s1JX44S+?aT(lC6{m9=W??($?bLZ#;w zQYNjg+(KO}!=Q`?{W_^}QgA{yydBJ3z#>tl77<`@gvMbEu7_GMS)-SNC=B=T}c zM4aZx_2-tUc_;AeFkLJmdI*77_qq?cN#dab;L^-_klW%qb>lGAM_snq3!0KheaFG0 z@uoI;K9dy+w77kllN<9Y;YpvDl#EF&j3(=F;^5h?zcg|d@r|z6Wz(dGH{-pxZQ}DM zcLmWCw%xV_dqg7Ku+aVaieI=vJ|2oGv(-KSW94RQ?$AIhO-vKnjy+Wzt~Hf0n-j}H za;CmN4A!{gaEV#Rk2qMb78|IgenWVQ{ov=y3mlwlnWE@hj7ZUY;-eB8kcx!JIL-lr z;tgk>+)F#9A-3zTPcT2VjuOc-tKigipfiJK*WhRfTxvZjRK?+sqEqnZD3Gaw0DHah zhVrQt$`h^}BM`$RJ05ae1}v)}|7uDKgi>N?S zz{%<1mn!n^A5DlM{Cd}B38`L4_C{Hb%~ba(h`6uoS+`p?Z$GT61zAJggq5u%NKyPW6^+ zGD1>YGX-0-s}5JHZV>3B2C~D|QSMOcNv>&iyd!SJY6B0?S~a&zsWUnTW7_3NC4tp) z<7X-HMjhAcWrhlS#~B{xeQjLr9OIWE7oqhM{S99UVK=?OSPi&;#l}blsAK(%Q`yC# z{3IO-cN@UT`R;j+zW3=e?|rpxH-DG^BbH}%A8wCXZcFeX*18;Rhsl|PBcoEJb@;(C zv2qWTKuC4;3rbZC))$K zud%hG<;eAcg7T>EHI^w~h8~WyXT&VoA$zzzU2R1qs$th@V_A_Ej<|#1NQ{f={T7}* zE@jF)qB^38L04iKQx3nLC=g>l!^3fN<!?T%k zWoxBiV%OP8S#17UIUh!W8;SMPpc%`Ch4D6|Z34b8`1Zu;TwLAUpG$7Kw6YHcX zy;TehOEkUp5UVV`)wEk99QMW^9j=fA(4e3eu*OcHPO%T7rJHa`hFOK#sZ76^uo)o$ z+d>GIc;Y@XZbF0_9S{fx;5xR;?O(uy|8cj{|65%|z%F*H@z#UD0<} zWqQQFV4})jdq?WkMtAi`)@`{7_U-xR_Kw`yxrK-O>psHxj5i_jVrgRK#ep)-aIB5_ zd+BZEd+F`jc=?u_S|qMlzycp?q>jUAY^Wal^Jn5USNO~S#oRkaS=O&vzF}tAwr$(C zZQHhOW!SbOGHffukzw188}F%8r_b#^T~&R%KHM?(_qF$0W3Tal<}>GSDiyu(Qa-be zyRQrXJCWLM!Y@ZcTW(kJ+i$|G`T0hT;1C@}Tgfzi4IW)$fz3^hO%);=jpI1bXpZ3I zib8`-ms=2Wcs6BjK_qQDOyx=@A(y(?CRe^_jrK^Xr0`;`!!wcuzVMhujROm3ILxdYwnQ_Xbcb}Q^LnB z2&u2hZJ(6YHht(2jie+K=87;PNQBaxMohhxnj5%x0((k+Vp~9-u*$!TCv+)e>< zOxU^fF6;FyHK1JIte;_INx@`eV^|z zjP(t}pptr$Jl!Ksjf|d8yxp@rq#-_Y68Fg+ENPVb9;6DZLLu}M^Cb+Y6#SjAAyAo6 z?~bGH7Vv@9GPG=^A~2SZbGA}mjls&r_R5A^81{EIdtoq52O2!;B|D}LfkC?{3uB?b1p5*J8Jh|^$D zAw0i$g$*!NhKhZPvzs%TigpnD7%Agxs8BFAOJ zuA@;pZEdn@Vlpv;wKe{sn`NL4&9}4*_jG^X+cw;#f`l)32GL0B!1v6rneOb$2}GgH;a5KHfZMN zpTz5TpR|uoa@_oqXTsj2pT^|MVf`IVhHF%(Iz|Ir#|ebiX$tZ&jFShVoGi;?VQj1J z^mCzAJ#iprpoGzcUhw`i0*xI3O)$coQ@^A>KX>*yQqw52M-s=bDeLQY=C*F@83Fg!w?A8Qs6k4R@Dt0`0%Ryo_VxMXVW?Z4BEH z8jj}N2-jsej;&o{KHPA`sgHon{uQdAN8=2wOIFO?Ew1QeEqRga#J7wl;Q?=tYY)ah zXS-~VoJ2l?d<*5r;a(j-wvlw8TV`AQqx6aX#7$#I8R}nzy**gnIs5jE>Gu#B;9aO$ zs4E{FmspHQRUlPdmGg2-wyNN_Q74npz`WmM*Opc4Pn=9BrL7?47lc+KDvDcktq3BX zi_b!|kOPX#x^8sNm!to()mWn8os)#hkE|OC+ep}(d){Ue)UamN zMGXBgG0oasFiaUzBSgw#8;WEuOEzU%QLw~*s4>~y`Ay;kQVwJ9U`Kesm^JNs1VScq zw=;WcW+TA1j;8*K-gDv7O0hVy>={PI5;{U}dGbtAr%}fO3HX`y7-*%#dS%>DEU`s* zFBdZf=4<(5R*2JFJ}MT@Ej{QFfvj;(pu7yp7j%a<9y=#l*n;p7b#)n@%;J)s3Q#@A zO(R26Kyi=&w7R>5K&SoP7}_z!{)`9XF6#Uv4eCmXj7VH=yd5@!ZcgMt==>rv?m&dZ941w z1q-k|$cv5*TU2U)F&o%9>VLsE3y_5Eo!AL+$*SDzb-E$kehV09IaIfYH~=Tbk6+v@ zATG0!|5HCk1}I=*1-6;Mc9%QS0>VC6{~})0+#D$Ma-y#97_5*AQ#z{24OcJAMLS#c zxgDl>oz#}y+9f(!N2?B3$`fD$S5$k<<3ghQQF`~MM9;9W|7Ee~PDyGvI{sM5yuesA z7ZKqC;g5inBzgD`o@+6i)vPu=*5cr@m9hFA`ZtM1FgD+Od{_w_v=r{sq0%|Cwr6oXW0%| ze+znrR< zUgPlXtjIQ>Em;=1y_XWdL!{1r)^SM{w7@heC&yU5nv=Sy;d8@~Ge?!;DDOZ8rCpJ| zN`+hc1*Y&JQKquP^d8-u5I6hn|gV6FfLS zsQ#VlJ%8pavQYU8kHj7t&eot$N0G2Wt%(Repzl1eI}*2uA4ibs@mZ#ZbfBwp7q}Afv^roQ#~AEW-$1j?r`6nn7k z*Q@S}o$#8(r&+!xmxZ^?vqU~F?l9;g+N3rNi1}`^J!GTnk=Sd$LC^ye9z3@WBlTtp ziGrcNwc!>*J^^Gytu=~?ml3G z8EXR*uqK$HLrPC}Dh5bH6>;;1MiRF4HE#7-TTCfsj>>c$zS>ZiYd;WOxY^s`XcjFj60vr;6AIMbLJP*u`j$I>JX=yV^57>;M^HDDX^JNMV(YI_Wgro@VN@KpV z66*3SV-$3ptHMI^e7zaheB}sGuu!Du$ftOVxPH|-HE%97x4TUju&wSww+D{^5c2w+ znEfg{HIz^x0_5-H(sbXYvj7$!Jt*6GBdk~?AEIMVW40@nD|%t};B9Tc?ZCCN03P|P z9?I3f!d@7ETcCsWUr#F;{}8Hv{<&fd9szzl94rWxA|JI@EjP2d8Y^Sic>+WogIK9> z-ud-)`DYW@?jyX_wmov%6jT<2B)`@8Y*Jft&aH=EWjNq90GJp-DjHK_NGbVz=nW+i*O~gp@|Vg64QXY>eMNLOR3mWzI7QRZl%p3d^mHq>fUJG6aDD; zB=}M#MP%lF8WXA=CKJ(1`44oqft-2}RXq&GuUMYr9a^YQ3+uQ2EuJo;uY4bGW3ILs z(}FI6{6yq-$fbA?DZ%11_Kf_urbex{Xlwf4T!k)*Y;qc-m664I2@?utd(wRPGRRry zSglRYg?{5eQaT|qRh;Qpl9E)n=C13A+}W5&4i-oxjT!2K8CJ7wQ6d?t=r%)%1km&v z#`sa=IQR16!X!y1vncN@8Sta#H&Rc}ffH@qkH2$Ydt_<7B+Z77cae@$&KrVF%B*1& zN~Bo>!18f0KHlyC!#8=G`A*hl#^ZhW89(m}=Gr0e_b)%*Ujzvsi24{|%TYjIcKhzl z7e*pZz(D##N#i|eC4v!&G>saJAkN4agTetlDv z8x)iz|JRX{?VmE9|5$bX<<9&k^cUOTQDFZaEl2S{2$JdZ2uCZ z{I?(fpU#nttjzTPyxuBO)3RIVMD+bGDJE(T$Ium)go*@`NeXfexIAnUed^<3x3?y_ zAx!9sn797?nEoMhYB({L0~RhFF|XZ&;p=p}lfSA>rv~Hobh$6IK&{q#kVzwZ_>lVH z;OMIQl}2-`jXuO!C}NwwB-$2e!iuDb#?f_uaZ=&2ntHyk)UKTdcWx}}mNt6p6?$zLO+VVmI?-dzu#L^7EI@>hPd)ZrIz1EGw< zvA54425-Ee)&lS7J1~`)op^bn3FgoPujVz~nDoto5aWWk3)g&Qh|-C=`ccU`LdhIt z`J%e4-Z$MVJwYDD#Ps>s$vy3}2~57HY;-{S0X)+P=y25T(X&hewpp9g3&C8Zo-fYH zbg)B3Ev#diH!Oo2LEQKe0~%v7j;8*6bkKLu@_iYN>pN(v0e*!+;FOxFyw3qbvT&Zy z-SN3TH-zRtJq98@b6QeCR; zTo7}=a10g}=NJ-GDNj)Bc}89@)ACqbMDDalJ%r*4R&rCS0Y;)Fd7sC@QKMFY-{bd_ zy{TZUz4xkqv0aUQcl)}04FPx0SAhg+y76O1k>YWm#h%Uy zI2;8mS(sm2srH5MGC}a}97B~?M&D_)6LsoOP*r!vMw?Jhrl0bT@;6@GDvtdsI{Gl( zB!hN?oeF*HSHVvtxDIY|+ctd~cZp>`?M@Dz&rbt;yatu32%uWaZMWjEFAM?0x>#%5 ztH`ufHI8m;-zuSYG{a`bb`Wt$#Z=pFr>(IjvEj286r|)33TxRDW!VX7RF|@x*f`^d zSt|1FCa5`?#P;l@3=s;2FZz6-W%cWfoLy@$6;=Qj)UPjq;Ss1oMY@UtenTLS+qVQP z5pf$xtr*5#hk@-T&m(?c=l69su@F?H`k0<6g}M$Y68rT>m6M|Eyc$V1f}a{l3(495w%nN8qeI7pi8AX{-U5; zCgSI3b;QDV*x=$hD!gA8By#N<0NoNr5J_`))G%0toJ@SzNGD~JAY}f@33+8FZ61%f}fClR+E0H{)Q&isTL#UzC z!t_t*%X+!hAy4&`DvObXhQVF<&g{ANWzE9%KbX}MLwZE8#x5nAm8B?h^5D=5`CeIv z=?km-FM`7>g6MKapY1rX;=PK_zxyB)(iLfpGFou16F3W4x$kEaBLgW@S?$iq1R05_ z-`Ki{J;Mg&vD6=5_u;BpzR!mX8nxG~_pvMk6=jvWr9SH#f-s-Wz#?1*DW^ihP&Sb@ zl!AV1zxM@2E-HtG`mutCAJ+MxH{6ny@w+Z#ysv3Q5=;f(GV#S2+_y>J`qq<*s4+gO z>iFwa@%{T6*aLiNYA3=P0vRLIsXdd9W$T7h%_a6ZF$bCfdR>jSTQ3?S3tWogyfDeX z62b}S9kfTmi?Kv|Y69;;c%TP{>EbLFk^)#%&#``l(w1&g6Yl9-`V*UAz_uzDwfDFK z>#c*Vugf&(JhfJnN^;)Z?C8|C!a(y}88WVHjUP^PbUO@Q}M$WLCkSqlNz_;-PAMl85jZxCkd;MkqJJa6H5b zO)zU+0^+jlr>b=el@0Xs*R38XQ0l=&S=7nGTC&?1~Wg9rqp%G#V^_( zp*?UH&4u1M!m%58R!?BtKk}7oQseQ9-N44~hsP=ufKEgTv}#icRxi5O8Esj8%vBtrJ|F(>N{@=*il{lv?!2 zGyUg2d+nikg;zD3-?_hkEuNmnS|Y=et21qE@y;nLSC@zH{@6DFwxJyn>cFBSZ%l_- z5vpWq`r|BL;Kg~~&nAOztRJhf8!&5K!@9zC#R~HfjP@99XPv3V3N=g*!w|1sPRp1u zGeemFCk;%g=mNcCJNL%EqpdByw{;%F)5fQnf2wmV@CMSy-vAs7%q2FFRDqqq(0PKH?AYg;@wcYw^O0b;NJ3j`g2rOvAPUmV;2 za-;q4Po@9!3X1tZL9;0QZ3V>$q$p4i00;(v)&@-cFRuAdp{;+63V+XR{ktoU?f+L9 zuK)U|@OQY3vhqJhg@1F!{XMqzAI}Yp|H>Bkw>$B_e?otcYB96ZGyn74(4;2w7aF+x z8x33+mXFGyMJytz2?CfX{S60hSQn4Z2ucbvT(zOwNSsqGCHwNNj3j(8kQl>d<=<$S zto3|2UD~@i8#!#FYk~24xqWn5KwclHG0s|d@iEo2{nJ(cWtc^cq}iZsk9@^ z_k3$@q7=22;*`@x;mm2&uMM9^GSO#4-<7CU$uYmb+8GEX?B`rO2EJojo5(Ri;1Nbf z`O&UItlTlGkY;!VDf>f4O^V)L=rq+KvJdVcE(Di)jK=WAqxf^NT|ePFxXLa17{O>WWF;cE%NUE9F<8+ksmo(#oXc=2D(0 zrbMvk>mB!z4=LJ&)c@Q5DP@ij6t=oZ-eK$F*4793Z=I;bMiH{Yseqd0hH;%)AAjt& zd;r!qrsMBrHkls=!b{g6K)<%t0pzCvL=lcWa*KPGT<(ZcYnI1{IQR2Hoh6H};|YGl z2PbpvI^YBset_iAJQO=#p*U&}*N>VeF!vWh7+Gp>{Sl>_I%Aq;I{ZU~BGkd6JOG6b zCK30NFEIUG$|7E}%kBl3i}<~N`ep;4nUNEyNq%>-kmqz{&~4;BB`kIojL<|Q{(u;i zYa{omi*xrU;tl;E{!P3X^#-$CQtO-ay?=r-#1EG+eeDVwBUC`omnO4xUgC{u7AOW| zU}GTlM+ORe2pQm1i886dAMI8cX_vZ`Bs;Mai#ko_E%7jNI zDWX>62}HQD9nzdaIzC)Al&o+;B3}%np9QJ4t+^cTl4nQy}lHo_8ATRS1_ck8O;>m3X}pKVZJ-Qiqw%yr;tZuaS$(R$oARX10?(Nddn{=Gh1 zL3xOY=Wl4kveXP)~t;EcauXLIHEEdqOtPjySPU zU-suWmF8B68twpic?i_|G)t}@i`gMN$7)frT&rNjes1iHn`syGxAq_vHY1v+J|2x8mAN^tY7_~JQ?U_AMz5WM zkSNn?y7c@p09cw1a~S-ALiWsUz#UJv6nTY9vF`SG zZe{*@J|B<;u0TAfg*m6B(F@*KerX^b8!_60w{7Le-MQ1yg*P3WRJV4C;KRo2BJO6q zvoBI-lZ5pe3Rvh)^hF=a8z|_ITYf*=liA`zOSbD!Zqq#d(rh<~JX3A1eZiGg5omK( zBo-Y7#Ja1m`{)mIK~C75`p}oiMD1LCzYU6#L| zU@iV2gcxH*^`>JvxV^_?PespIbNp|TLkt}`C-kP~g!L1t4-n63g6(AGZJenD*Mmzw zO|RMMK6sU>GVzo;A&!+cRmI#7T6@lk2*w)4_d36w@@%^EHR-uEtV5dpHaZHlz^FUQ z5@UFzmAj2KUwNc^e|D2YV4O;`0F0r1pg5@x!uxp{>s3xc6z0(rk>>Pw+5qk^iDh#} z498#T!t^MdFMK3(ZP3oWvkTD_6*R~|qdw;)a8I_-A%N8e za5=jbAI=}ZXiS7M4`uFPG%EJ@N>$``D)8m|@S}IAl?UL66om_|UKfVdPbx@w9eg+b2E3R0bjYj zO~oL1y52hZJ|yX2S~SQdY8+N}vvprx1 z9d(%Q)d2FDxVY&xW;s;Bz9bAc&P2(z=Ub6aw4lcG1!EdkltUvEBa~xXV0BHfUM5m@ z^l+4UtQt>k6p$LKNTXih=UfK=IGba&{UG+tiQt{mCY|8SGc;(_h@!V@F!L-!p@bRU z5$raMb6@FB&1mPP&YC z7w*5_cxAjd3!~~}sk%T-;0cYDkMCzMsKwSgtvDhb7P~_mx)gLDx`qUCiraEnIa@4S zp4##V8vt4Fn%3PmvzQ^ zn*^xO^cMBtyly{N6okhu6V|r+N-BjlxUgGxrlFL&&$Z4JKxfWkwRj97WE%@Qovz;z&0|Wb=KdmSxfXGFMJ;-x^74L zobXIJZ;1)o@yZPp0}a=RScF0bU26gblIFU|Yu(>nnO;dBd?bD(_IP_iqeT=Mr!a+; zK#ttKrTYUkODkx7r4|}zF!$+b!VJYZs65<+g@Xhzy;{_5PULdJFZP^t5I^59x<7$I zE@vniFy}igEr0mvR#G<&Y8snDw7hxcE)6CXu=)lIu3+s%7fwtR0(m8E;yD#-=h`-< z1NU>av6^f}Y0U;rZ%fW7L~fXF%CaJ6G= zy#?G>QdttO2lmP0nfP%ZT0XySc)KlMw5zS|4^M{|NaN?XCp^ABb?Pw_tF-W{E4>QB zZr=nso4h_ThGFmC{`A-{@lTGqW1?pKuzgkc5;zZ6$qH=~IeI6V6p80%RO2oC4~%S6 zpf~yhhIb|0FM2=eYN7x@Zn%ijFF1nJGwl{tNn@#1v|1i6foB$deY~h})w-MMO=u?O6$dRQa}v1Tdh$S}ocsQ?!*>8KIa%9ojm?TRNgepdzs zHlK{eWS))br!v=I9$A*YUy3O^U_Cw?GpQ=+&83Fnys^pf_1dsMVKi%ah{#YEJMsUNl>D*6kqi3j2r4^bveLAdDL( zJ9mX|^ZL5bK_RO0SIywd&=!k<64|WC)3^l2ePU;z!BRMgUFz`kwYOjjuYfv?n$|fs zeCgX#&lqncq^&JA;>PRi*eInr2vhHP9MxgU%&;V#aF!Ac#(eTn3P8%!Z#^$S_mDnB zy3k#622S$>>z0(jZAs;0>H{q)S24MQGN71QKXvMRrZlW-Edl{TRac>U=wWR_n5Fue zKyp9FOjJ0_B8z06=2hF7OOc(o?jlP2GXkkDG=8Tp1PcPlqu6PP*0GA5 zbZU8`A4^0RFLXbHMWlsRhiPU66p24rj43k-C{GcGEo!n-WUP6WuUV8RN|Z}>Du-wt z_b@F9Yv{Mp5b_Poh@&t!C~|HUHswTq&e^78wM`wWQ|$5&3C-xs0Zd#TztpYT z1o2u66(g8*V4b3!|0L^Lu%r@&PHA|Mr5~mgVWD`6*^KNzn9Rp!&IP`dj?9Imr#3c# zyot=>;S1pJes=@>1c_w}m=T#RqjOZ~U z#C=|o41_$sp@@3HR<~m)i1*iyk!CHx?+geGRk)Ckaf54_Z&MP87nB|%pF$sS2_oXE zLUUsgr1x%J|27Lr8t}~t*PZOOq>E2D>#R(ga`F*aDjQjCo0`NwoK(4zx8tO6o2(UY zV6Eh(()s4hvZxIISsQ8?;Aa9c)f@=?s^|qN&RsZJVaXw6NhUg) z1v}wyoC;eCMls4Vf)S*&b=E=)W{cR@UItE^z&&?}H;UGuZ7hk&QEjD&qC2eM(u<8? zFN+)}-G(xdXaq7EIs6uAtI|}tXnI3)&ieqOB}}b%Ua5`l9|e<-PO2;eo-`)q;I5Rr zoHY$+(bxkAY;A(E;Yx*={v%EfkaiX4SdSRdXf}@tzPSsa^@L^H`J{r*HpUigsS@^` zyhh?ec7R;(c7iyS6NE$L(D?h>^W;;IJ>;Lgg1t9AaV_KK*!?C@!KUxMn>(cH^a-C% z91~4B!#YbSyxn4y(^yejZ61o*uW(E{B})|H^})91`Y|O*71H5Kz82asHKZ-%7pS%T z=o`(plQo0R)6;obs{yS3+LyK(2y#D{6Rd1uweSAME4q~aoWFYR+bHB zcg9`Z&56Jc>G8gc z@9Tf{9Te{VP+=6Ii)C;{z=!%EVH@hfqjGypBL}TnvX@wUp&j7}$;AJSJ^i*-Ftja^V z(vsN~>-x}xa|?;x*3a&B+tNl42N6=Uy`@ZV9NZTI zj^I(Wc)x7E)Y10O^@i>%R^l7$dpgs%s8bg%44aAV{`~s7`5CsMaW{o{Pb|@_Js^Zydp}y;WbE0NIsZ$7nFdjWEh6tpLKZEe zxYDE_l!RM~_<;C@^ON1sJT9N&H42^-ep4aFBu4WusSyN9c-Do7O4f5KvwkYn`m=Bm#-bAvW<4R0D(>Wx26HozY0f8fW_6TA{BWL8q91TCfw0uq4a0ltvGOoGIBSos zD5Gv&ZR6ZMAVuOMRe^C#B;w;twoj;fTt5Allg*~$Y`N$V({m><@EznP`mKk;r0OBG z?ckP8gl$1q*?#D>xB{BNb1Dke`1B&Rz=E1WixAkO+G_YulFNplGI`|)a*A@3-^iF704E2K8M3yxi=k~hs#gSXl<(rs4n{=wD%1yJa_|kWy{>)^o1%^eOz^$@f|t0 zgAOdvbm{hfyK$d+AHZtjmxtWmht%SOZC0JZ91lN!r~Ki3{(#V>ZgB!CEvY!IX zm5<`l){V;uIOw=0Oa7aD`F!*1AxMrCCwK+}K2Z*e#@;r3xA73TwdKKdt9yxIPRNE= zgGLYUb{1emIqPbL0uk}p4E8ryxI(X)N|S$`MMZ>ftFRwogOyW^@itp0)8E?BnA+Cy z8WX8Om!_6J^#CR@?s=j**Q`x1>?{Eev)W1%1@#kp9K+krA#Y189gh>k7FkHG?+KwV zqb*ixJZ!3y;7+@@XPTSH zqr|x~sTscTz!aU~4-&k;Vc{7Zxu*r!p~LKJoqu9PQ|AdLdtkQSE`+PcryV(KGLL?RXvi1jgDrqBvuFy~I{UUA1#PpYGZQV)H>Z7UGQAwK)pn)$lc2^%TGCz{i zvt8`@XOFNSUAFZ-G3+Z1@Cmh8U_ROkI+$IqBjVc`qM_^8XeQ0z0)=N zm?#Jvt1*gcTDxp7bN6`QVW0S&a=VmPz-E$_Ty_4XQ@K~O4s={R2rM^EUE1*hH z7`x#bftE_SN`3H6dh>QGH~tlTiwfFha1^=>y{gG)t|N399%^^%&Y*dvV>@R@lTA0M zv~AR?FFW0bGj;P;c;`Lyd7hjWJ1)gF;{vO#)(fpi3MM|0tD@wR`)51#B>5*P*SEg)YpoV7hO%n1<;X!%iy#>8rS=9KQIPC3XZ}2$i zkEbBfWwa_!%bNZ?nR=$+Sn=ZzFp0?#daRcPT`addqOZp@N+wT{6TAp59KW!fF!XUp zvF-V>gfFy^6kmNp!C$%2oEJy*K)M&i3?PhCbw{p{6!Huwd zde7}H!Y`OuNW#1Vf#_(5jO#v`<2VUQ^BbQ8fA={Jfnj@s6EI(k1aBO_D#SNQH%#j> zokQhdnLQt_jV}2N^Nc@@b`5@sVK6}&bNp$uodX3c7^t8^ORA|ujV7&y0MHY$h;TiD z8LCp{q6}NOL+&h*JAeHjJ?(n9IKE!RL@;6^^+(d^q%SH^^`N!4!Es526oUF1$~qf& z&-}T_E20>S-Wph_p^)lA`pTyYwEOzNEe|3Xk4#eTWE0Y$z*vx{n~+`!Bv6JB(e4ic zp>hlYw8(oG{o}r^;Qc9OU>QL#WP&AxvNr}TJMpV6*|>A8&oqYXuk!2E3jr8A70RU$ z2Jx7$ON;TFZIq<;wXa?ZT0!C4z95Xx-~c$7!0$Zizpjm!T!<0psn|IYi?pB1C-`xd z%VLHcwX9j;&TM|3q5-?fOUDvN$j=8=u^G773mOvrd~jBxZr#t_pZXIU%pNaJ!A(SX zJT32Q4qJSBxmfSWj+|hc9li$q@&$x=n8zx&^|_Z$5geq=RnXTlp{LCr&@j5B=1EI^``vkwD#I zd(>UnD@XR$l04%j7Vl+WHB6HXhLT@<^cwpxU{I{WnnCvb1{@TdgIZ{!sD2D**<8xp zZo!jh+9DaPUU4upsod%PlS+|Kh}*tnl?t8}4=x4|Qo1pXYB#Z&qms9l$G$^eKCo0{ zT)wcm@m-`DK9O$Pql&kdqr5|0_#wsnONU{|a$~qdUZ|J6V+$O9vHRt^V?h~s#Bv(h z^|AX09m(8UmN^wy{P)q+pktm(N!qD_#@*+T>??Uq7Jf%~Yl;X?S|_^q4U9k7x$SHL z#XBIv_W+kg#Pb|J+fd(!*1P6^eSi2eY7;lbZHYaX)%NSh`AJ8&eRBc;-;ASOkY4qr z9HoMA%L&R$kM52@GgKrP(UuaIP%*355^o3t5$JT4LE;byCdBLOip0yqlV8Ql-TW)W zDdH9xA1;X=Op0FOxCVrZl*J@F#dSiHMWFot#3VO2TKv#kKuDAHTQBY)qH|f)B*ijE zwGuRH(s9V)m_rwm8aOwfjTA0AB_A-KY2aZStGh5kXEL(>xThfNz;44eJUXNehc>pQ zMZI=O7*j#6062zlg%^rj{%=eL{@O;4-(6=_+rN|&S@QuK=W`RH)q+~C-)&55>X zm9ZRlwA>6WVo>{XlOQmZn(*;*>;Clec{{2puR={SR2{DnbL0ZZFv204)5o!3yo~gt zT=v<-nk6LWTzSP2r61`NzMl)xC-VlZtXzvd$o-;U|sASncL%a~>8;40ii^}}YOiqA0vM+`dLNU)ev`PCr5V?pR)1-FfPT%6}2 z0X0BXO>@&6frNE~dYp0eymjjs-U!FgC&R?w-ifnULG1#K>>gS0G3N1@a6Nuo3SvX) zvC9K@)&TcHMsParPATu~<*+UcRs5nI!l_aF1j@oRS&ulM^4OdujE?N(Wo(g9n?A$o zPE<8TQSJt1SJCpJ%`*7Z_Or`-i{6`ZHbJ=UM_n_EeL1|Ucxj`K3Q8jo0W#1I`MHHh z@8p3XSz1U0jT&Pih5}X(ZItF1l$h2BY(fr*9zrxdwo|}TP7K3DF<~@eR{p|7k?N{C zQ86${eocb%5pCvT`}TXAEiJruUH0oup9O&!2acT>T7QYdYU+Ngpl&l%7b*&VBXiMo zUcSZW8OKPv1=Rb?&0TBSTVH^+0Mb-X7L^3lEPy&a#V&v?YmasD8h5vi*4l=hR;R~m#dEF( z@*kFEt%#ZGfm+NjKAkVls!lV{!ny{q@h*)u*6NAGRS)lI*TxT!OZ?*4hsnBp@+utf zARBGqz;CY=8rl!6CrE^4NZ*Q~K*HTW>y);PG2X^P!!i||O(_yn* zr8>T)qOe$(2<}q15hCAnJwPj{$!PjptJlU zI0jwWLxgHcgK>NM^6BbM3*qMg?Gje(kBvVBi9{;qmpD#bZUWq>MP%pfGgDX)UL4t$ z9q9tkfJu+^7XX}vLoR@dFVd$HTpuv3A$stZ7XTz9^L-nkn;N(~(_%vHEd^8)$bzyH z=xo!DK>cY{0H}kM|CH2}Dh{=kBw!I^VR(P@$UyV%FS z<0lQC1hY3cTq#+|!fMgA!yCRim^EIimpf<&2uWV~Dr>Z(T92=XFA|5_PXH|h@Fny{ zXA9_#^8}cO1qy;(E{wNu0ZQGcyGWx{BnG??ZX9P=Cae~^{>xVB9}V#VcNSw|o zqixwzbs;(Tjx}WY_HF)T`!VO0Lb1s*;VvS?Xl&9VTo(^2DvpoP zW#i$hlqePL3axnc?Td>M^`T^95*b<7UEC1IIVIF=6x79&SJ|d(l+m%9r%U6E1H^IB zBEw)I4uR&IGb|Dh?76`$`^RJ3;t;XZa;wi()vjU=c}f{f({s|rL>K;OK{P2xFaB+x znB1T}NPrJ|9iT`I>s0Y1Kc3Dt#IPs!3~fC0RL+6Wm${?SOP(+vdXB+Og}wYUWC2Vp zS_ao^g2x{}ol_Yq^hJW)7%IO|*r)`a5^v$XO%}EZHGf;yHN&xhiGJD4^6Q>fNqroh zSakhyXPP+53AvS>rD*JyV`#A#cNh*?&g$DY{zLeEP0_o)4PlCJ{W}H-?S=a>jP%on z`0_DwR?Iysr|?;=7!Dr=ks%mjgA^bOcjqZeJK=(No&y(KQHV~%HTg=;5lzmeN*srZ z;vh(V%fV3GDk;2kmD^V9`%QN9=dF6H%Ub3A>%!{2V%4|flVxtmAk<+YH1VMG=&LS* zPoE{AOVkL0ZF}-zlTj&MXR_gCYa?1+Fc`M@5||yNx~)fRNdPefKycBBA>xswpCoCd zS)=yOD44WQ6-@Kfap=OVeUKsN=Ei_o&sf=KUaer<1&ldzy$*T!c;kE56f37S4mIx# zrUJLz9d}%0^a4^&HkVYCq^(JVNV%Ilo|tUnShr)tI;ZXPvlVp0_E-0AG3753 zD$HO5FRB?gYqBNG04&%tmex44T!!K#dr|k02CYtBlT~LKeyU(nq(6F*OP17 z?Oz#y|8|T1a|18~J^Mea?M>fm`?~mFT@Cj}P7IcfbX}kV{wrhW#`S>F!s%!;`UFzR zQz1f7$k~H_Jt|A*q^pP~CZ>DfnaEj7I;FDN6&>E|u1l`Xmn&z>X?C5q&A#Mp=Z}u7 z=PS8sYHCw%u31NdQmGU)$)*vEgb5b?4a7+Gl#c3;FDEr#TPf$at&YtUTCVLyrCCcp zDzzOI<*Qk2YF9O@Lrb^2x35lL?f>X1J%B38aWdqzooA!lBiGZ&oe?%FNVxrKk^jO zi7#E^est!J4Q=~SzHQsc-+eKI&GqsTEmF1~s5C#!@CrlrD_J$kfB8a4Tjew1Xkv3q zz)4ebn^BwSi^bTO6S$ZwduC;9vZV~Kc3A+#*d_c1kb%5V&|v)vMv zyv>s9+sc?N$F>7ENMw~p$3>&G5K4(!nZ@dmNH)xn6-hL-em~kHr$eM++@2MLLO~m# zy-Gvlr~PgPHgPm- zRTvOqFgdN3neQB!J5zS3js1 zCnFj~7^1GV?~X=GW8=y(oyh)^{%v6~>IcFA6Nq^g3du0`DG_Ew9e$;=`yGqh@O z=^21hS6a|&=DU~rd57M9B1FPced-Kb4gw5xE5PJB4#(0G5NgXK&Z|qDWhW@Ej0{i2SeTHNRAXd=VfkST-_;6eo8m?Xu5PS1V zb5=Cq=gR)h+D55RG@Wms`QBU=;4sYg8ZMeQbaw!X{gs0<60<#?WI|NE=>|}cs$!xs zE%~V6s;^aBuk?%S=Pqu_i3V4mQlu4$=qxT2`4(jaj4<;q%Q?Rwz9cqi?gr(hYBK`$ zXm|C_9cet~Hc$MPp!hRF5BQEW6j~X?Wh`iVQTCYiYSLrdLvg~DV`$Vb#`46)jEVgh zDn+!gW<RASA4VaiD_mQlVBB*)p^w9L=Loct}8zkk<~a#V?$(%>LJqs>SsAkmW04 zcD(`=F`w(~*glXT9(5wdn9W}MxzzoXQ~MXH4j{D$5~u*`f>R3r9^#K+*rso){B&3!MH2EG0b6p{k(QS1k3$-X2Mu`bO-H0yv=pSSUf$)b&Dxjf3K>QHB z$tD!H0$2g@s6wolM3DB?{e2XUSu2W6ajbPLVU$OpUc!}v_o2-(QvfB#jm18=drEeq zs<~Ji$djy^1^wecJ&dP!*e*1w%5IWqiTGtqh(bj5qveDiS~(}lDW^%5ZM-vN0G*f> z!Bd3R<{r!D6%XkTJo+SBZxHon4WuQ65x01L8e9`29LAB!jk=F>y}STw4cFmRLiNib zdX-QFrlSc7v4>nFQZ`sbXU!O!)O)VUYj*>Ql{Lbr6AwUbJMj?z9Gyq-91blzo{`X1 z_ed`z*PljMtM(}9aE&&a!rXLrw8p@^Fh_4=BmCn(Tn09jHFX(50%c_s&sj*yGf^`b z`ZJep(uyRan~`}qtbWaQd<42woZn|*B5n(V5moCT5+Q>UyCpTpArHv`u~PJbrLip! z-!IV1uZMuedqOh1x@$ISZmQy0R8NL}D)-1z@nrgbCYRK;Q^ql`l48dAsOFXF>gwa_-shsXY`sISE~@Va(nZJCW{t(knOt>=bP-P?Hq+7s?@i2t_q@~sG{9l@do z*-#FW9=xWs;HeqBNtwTdieC!j7#JQHUaigGNCf+)^!(WW@Cc00hC(&L!t(R%0vJO~;bp=YM`Ox0Ta5{A{B*S4b- z?ZDm9N(RrjGC0V;tAzn`vbeQ`#=7k-lX7C-f8VX7ilDEhaEjVWZ~gU z%T6n%!HnYa^mC)u(p;}YzGT@5|U!9{GV0r1Q9Y3x>@C*^?8|s|v^X79zxMuJS!bBdC8@Syu zhib{0W}~KVV52zPcwVt2&g;iPEDGtF*61eV8Ozgisih<(ul#m$bSr^1A;OP!~ zF<91mT%t)VqD>X?q+m9fmWVEqff-fIAGq=f?Q;vf0E`sBKA@^~LFW-u`@xFK%$sNw zF0_=D+J3&b#M;Xx8)7DhaZCN#C(BM8516$8=ov5s@UO=O>_M3WzTv`+eXAcPU(a-! zj+=Om#?!4`_Iuy=H|o&7swojrk{onrpkd)XCe$ySp03j+eoupF+Lmu!1@W90lp+@k zkhqk#C|~u4MF~P%e+zgudgPvr8fDnZIgZqXWk*<2SPru;DUc7+nHA4@#cg_5*SpbQ zoQ9l6qz_&7I}JMu}&DE(#Df+As!mVH_}#hHTMzgii1DcUh`18gor9D)dOW< z&Xcd#Fo+uNM8b_s%~vWPEe-H@Ebvcbc0hGp82VHe5ElhD+CE>|5Y)WFsf`Uta!Jys zO2dxxUjXwS92{9}Ot-P-f=!^-ZT{(Zv@(b2iNSm7!j{(**PuP@AY{3SP#QpL(qKS; zw9b%BX7q_y?Y(5LabRUlT{pkehG55mwLTT47ykkWF{2Qg+6Vk5uX*?yPNdvheXfY( zxEUrrrrQy=D3Tf?V6Wu#cu+*2r>jwsmlRREM=j*pH`+PB@)JKK_3WWeF=yc zmar>ZTT%KJ)?!Z~dsM)#A#?6_kt_qMj!J}2=8I;HJUS`xP7hli0h26(uvGe)ug{`c zvN7Zoa~CQs_UZd-5y68srNMgayR9z_%PhQVCCuP1>;N=g?+Wu07Rne2H~0&86=vh< z_P(V3C!WM0Y&-FY^$Zx4g3&pw&h<12#waN6skp4$M++7#hFI>>oKUAZH(3LDe^gqL z1CuFVhG0E!N`}?BwY?L)K%}x}_6d3vbzsIhx)|M}KswJ_&j&wd56`~zKxbr!n2C>G zt{DIZhYQIIm{Rn@Zh(2brNo~HAUO?A9VmQQ>3^g z%I_k;wIcpHs+k;Ymwn83!~81im9*4@YY%)+@rHq+5d$MP>RbD(U7q8g){e`8PM?t> zzFJ_YVSjp>f{fdQH^|qz1kp_1ae;b9reN(5`%mot|IRk2c+@!v^Q~4?RWH_gBSsaayvRzYLZNix64j={ zF0Vn%QsT=xpxG8J<%e%Xk3E6UuF_Fjj*p~YmgW3ucJN# z*e`o z?H*OIL7$AKCsA`aJ1gzp+7`6$wMqlDyDUl$SM-Kz@R6NV{1Tc;;r4V_E~M7h?#D2a#CPR@M~(85?hDn1u3WwiU*HoyfgkIsBdYxnPd+H z6lJu92v*rzFvqC`M0j~g3|0-QlK8^3dbl&Yr7U42mLN@&CTu`%^#?~RU1DIZReNq3?YvkR?1sA0;eOpIew3KP7MS<NzaQV>~y3g*$k>KMrYtwC#O$M+3f-QupRO7(HFs^>sXVaP;nhJD%oSTIzg z`;IX2J^6T%6NOe2pV=(P@wd(=VAfCwhr=%J)Cg)Oc-HZF%t74o2+VmqcX~QXQT~8MV{0nk^NkNvcHSOnEou5~G+L^jZsy za3MH!$2*V)UwJ8EP5q)%2L1J9QX`~0ElCKK!+=McwGc4-&j`4#S(t>2&-A)~(-S-twY zd`nAv3}Yx^-sJ7`cx5E5#~osn0khQD+xwJ*jKJWQ5*umJP2ayoiLzsEEGN5xcw*mi zCUw!!Muk!(2)9uO2-B*M1sq_Bl*<<`FB`$Y4=tk(xqtGl(>sUPC)*Y@L&5*> z4zrwsQ#!o%b2zR0>k>osznIh9!+8Gf$i;IY9w?D$Dr$54|rg`RXZ;5E;wiQLJ zj84?hA5z3{ueZdnlZPRn_8#rpxP$F3$@tO3hZaf|gL}VpF9)uZ@%dEw28hNJ0pqYL z8HqDke~gNpDWaZp_ls=f*+x&d3u=4QCY>A5(H?)CySp{hBoAAwgg;;NdAA{<;~%3Z zt8E+9a)zph_Lp_EWK0_Z8hCpkmb1$ac#@hpnhtzO9Q=^8)4#C2Q!ijey$^{9`tUd~ zLeyxDY2X%a-4HFU7=vg)$sr;Z5bmD-c1KL;OpzYlZjjA;<)h80ONyLYMpv8(P&YR{ z126U2+3O8wU+QI&IpV~pIaoFW`_&p_qFKcn;MFME0~gh(Pewk(42lR0bNS~}5_AYA zQm`g2B_1mO#g z;#C^A|FmI|!MN>9aJ&i9K*rrs_t#~)DBtnW(wD-Ner)l(VPmwe2 z=$QvYCvH{vKcgtc%wLuzP!ISAIz?ZFWzlpDPQ1&!1-$UECa^sv-r~Gqp$_4X%)!YQ zgW~>6x>~3?JYz3~QpzSGY2_2kJu1vhb!&vL2Cq1$7zySz2qq)=(&p4`Vg>~-GF%9p z(e-Supwuv$KDd(=6_@q5xe7owc23c z0e=Kb0Iy_(jKy0mFf9VLr~YA36_)YfTE@C$HiczV4!o~TCr+cagg9V~kxBxk{V>M{WY@ssSw$`zObvJo--Wy9g@!pd9WP^so~a~a z1j~%)g1G>g+HZ(Z!!MLg6w0XrmgU?*Q3BteevP*K1tD8J{W#f7H`(GgpHhs83WeDc zJAG-!fZ2ttdyO5N0|?nOJzjB$ersEVc_vNt73S$JrKXphBB)hN?;CvD#|-F9JC$M> zS;{r5mq4|^>L&Egs!=78bnlHZe*}eXA&~t3E>!Qty@7QPy6W1swsOgy(**bsDRvCN zVsD+OjQn^<>hY>>r+}ADJaQ22(~;SCfeZ9#_V6xXMLDI$gr~5g-F6$gK}GEE4&c3K zhw%U$LDy!8*)&K!l`-J<H>chkj%2I_>>>akmrUwS zm9hfXi)w5*1VYhm9s_-mMrWPu09X&HP-*_#TSoy3(~Geu+aoI2qj3Me@KM(UIe|5nT7NUU&$S^Vx0P5gj9?`EEQ^FT#yEzFkB7r0%a3LtBj!2P%HRiA zae6Cn!RdKt9${XilUSY$} z;vB)eDf*?I$gz6Qz|~DV(hRc+G~rM^w$_X(i@^uv`j<@rA@novUF=0*t($}^H)eSw zp|y#SXKfj^ZT<>4pcOWI_1`3?I@J^QGSZf3&t6u72uNf`q&+ABgyvSBSd+iX=5;%& zQ*Pm~|K90kDNlH=cU?{rX1`AeeV0zeE5alenZ5<~D|oek6s|^8=P} zPrZ9b!torl2oKV&YNR0rv(5|x3Drml71ABnu3RIU z30R-o2JXg)wef_Wdx@~M8~AL@Inc4q#tfJMr!7mL_al5H(&vs4;@~&@G5{NEN-}*I zxdu563^U48(+K$N%YXEikAu|vVw zBmK;r;gNC2WY|rzaJ&&M0qt z5s$$F{r2JsPqH$c%PLV&J|)2-h~w%oA$M1zqDRJPyzLEq38Z{9|5aWWH?`O`kGzJg zOUWw}Ohf#RA39B2ieK`S+zyyIt>3eysDK26e5TN?$dkqlN8h2z(?sCtbP(NF?J1vM zOnC8)u;!&u6*`IPgvmFwOn$6{eAdQdj+Ib!g@!=UxiMd{O;^lpH5~Aa(y+s-b@x+G zDWTA_YB0VS1`^zRvYT=Md#pYCTl6iYz9Nb8a346+)7Bq}-zhkzIE}km`YYV((pf7{ z$4yLjlz^$}vH2k8TF*4`dHmP9X=mKbESPLxo{I4 z5myU0_G7AWLTy-`Zif%WJ5no`Mgv~SLh^!x3Aht{W;B=X%a&a~yJv962`q!1j)?Z} zN5;BFf?a58RCTmmyV|1bM~zSVkqbdQ^PA&4qqNYtJ*^S!Y|VwGbFLu}AKc&s5L5>N zv^qFWzTC!T6$%*LBM+(b@tJpWP1g;GXaV~7l#tV^?ipNA?1Sc^JP#$aFfRsj$6~*N z*0%J~jlXW)s2ZMjXfk$^DH6bp1>>w!)E9)<*5BA0c*B~ma#fqCFHDJgs2V*8ku|B5 zq)ug@A1c#v%7ibMAeJ*FrBi^l$ZA)X`4==Wh zuV=N|T3f66P~;#!i%t}ae;z``zml~`%;otNZYhh%U6lYkgIlJobH7Wy)z$Jte~2;n zTR%@ME~h|KV>R>}zI*ikiOa5o6ckJqQC8Ip4>pP#f^%f)s02g686Jol3{5V$Q@zz! z@+#@48hp3tWru&+@oJ9@*CBElwCWlP)d&R7Y^Uq@#zc#XU9N~%O{?fYG_wqN)35zcNs)ouI6wleG^u=P7a*=2I)asJ9Tle!nY*o|G zM0nCu(dL;EK+K?ZKBj0vp)~zw2Gd@b*~@!2hF%qM)q9+EYf0lB_qdtHG^mAW7OXq|kANFR_L40#l~ zy)YsvimdaSli5^CXNSyVmP6GI&6n4Q9ft0fhtQ(youaELNY}RqqR}XW-*vI6v$bjR zT$?53Otb(lIk1!2I5-u8)670dxMmZUfhQQH0fJ$KNUFbsQg|t29E=Re34Oq$3a6$X zZt(U}<5&qXeN#UBLe~|0sF(2ul9CRPVQhcLd|^6Eic(=Hvv}j@jl@hRANxCmWfdhe zNALt_3aKa!B1jxK$-4!MF{nWV7Ud7_QqVv3=Ph&xgRRnm(CVph27GL^ogj zVwzrxECJJ6eoMgSr&693e=vS{y`v3&T$;wU!=|Tg{^#gBgxH<5DY$x;jN@H%PE2)i z()?uXzUG>6|2}MsGeNHB=fw7FMTtNI#q!ygV9i7df2RF$3~sqJ#R|u^NWsa;Ba01_ zZLU|z7(ip!?Kj~rNOyC`wy2Jr#xsz2 z6`-w7M-Um08MlOy~^4{JzH57LjH zhgM@wlEr&SoS6pmFxpe-{gWLWOvAc^f@BySN7mXAH)CsLLye>rSSo{9tc;@)sLLTi z*IZ4Gy(cu6_bf`~+`lA$%XwXO%QsahKJ_7zsYHvSs!&2nuSj30h!S_XBCPgW}ahI|2(4Pmp7FEpbS>e#(3lPZp)P?F6amse!i0=$XyuA8_dfvvn?WB6Resqkp)h@WmL#V?hf(>HlqfV&AmtB8*i!dX3 zBG$1VBLsAlR)VK?rA=c0{sMv>f@q220~#jCv_D`w1|gf;vx{Jq!paS;jdAz}U%+XT zt3U+2TAa?-{iot7O|Skhbad>0)4u($k&%DaEB`$*^3VF^|INJMKSf6VV@j2U;GZDj zzl@2nGyKb#$ba3A{Wo5~%Ea_nUeNH*`P3{uft@b-khI-&k}?V?=2adm^mU;btanl! z9Gvk#j(nQzORMzvM>ZxD%CMx22Y0m`Kk7K9g9)Z-CWkK*7xiu0B=D!R#n<$H{n|f2 z%P-zMx2#tGeDb4DvN>O7(1-tCgRoYwsvJ>?t7oJOTKFAcjE`pn7qgDa^=;f(qQ>RS zTdb!RA$LBxS7XC^@pvAQO}n}}_;K-Z7hp90x22-w^!oc(ZN|Z)W>ZCRjhff^$(zxd zS2fkJGT)#%KIQn|O-A>0rpq7C)W_u$cREF{yyY*PRBrX7iAU056Ly1jd`I0`5)I_D zDBmScag*O}Nzj(FCmp=|FnT#`GUq!2yPA}aHXrepx+YBHtL;`rF9ykz%z9g-MN#yH z5;V?il+n@_+{Ma2YV+S&Bjw3h%JL@X-f0qHS*x)^hn6QWAR+>4P_%!FQwW28Czkp% zef)YPn-ACPl=DpPLn*t4n=9~4qvs)187m%xG(UyrInb=1Lm{H0XS4U<8?WC8MWSa` z5*#nPzq|i{=>f$?)Lvq+tgwU2Q?Y2$%G~^Vyf}6NNWX`S)nyJ5x+m=|9SsWhv7?5N zA0&trR8_$*^s!9qSE9(AZtOls2mqTaZ5R?7Y9ErKP|@d(rrXu$Lz+f=lYD>1VyG%W zv0;pF9g%`DOnvXrGhymar|D6;8Pv5n6v> zVt+-(w-A5;VYGL{cd5}(k783k6pxx(D^A3AOf{Tv2!kXD_8sTwdI5>mYt**W{*5(* z(Sa=$&DnP|+}lW76VMM2uuBAQyxcQL-TjDXGAr_s6X;kg`o})qmYC+5MZ9Ms-Vk0_ zk@5GLqP$Zms@=`Q_3R$uh&8+nlDs$s5jjaAzuo++pNj%j7@o>6wU1(j&+8&ee4@5w z?(GZTDZfle&9-Z~q+uHC?t&wOHg)6WMzh1L&TZ!L>`)QG* zf`Lp;8xCz1j3v7h1ZoVnCRht=qHJj*D;^y16GiC}C#P_nhC)Ml91#ji{o+m)wd(cA z&&Fr}8jSG1nJrEGR->YcdxK7FnxiEgvlTGQkGqBxX&LH!WBWQR$T{oh&SW>0{@i6*X2CogrFj*Y^l?HQ) z9jo=gz9<%z`MRQrw}jszU)>maFGE8&3;E zT0;d6SPM^NVZyU8PVAk7;qobbl{|2VgrD}yJ{riBrx!uaYe?T1WNwV@C6VP3cSh&R zah>TXzzI`3lh_Ieh)p*4k7$mp8npv@wI+V>RyZ5ZWQwq|^aX8oq9)ZUN;H63)i;7e z5gHwv<9_X|hW!r9h<>reO*$vu>N*0}7rgRKTcKV1(Z|MXR?6z8h^Dh=DpX3Jy8>2W z(+*sahUhRILCy?J7M4WqY0OuMpqcVRb?f=ZfCETh0hE0~b=VB?G@I}zN``g4IdBxV zHMyC-S7L-P&p81c6{4<}?<^GM3$h*6+g?{FrUWT=g zaS{@I*X(?bvX;s~;dqGM*PTS@_c5BuTdHz|H4rza>q09STl8bNgLF$kpRWgtm7 z*M?4geyNTCN|JXj38q=?be8h_LKOm-l{pLO=nRUgqJ$$^t@6T#5oCu{S*=7Z}t3i zPj%>QRR^&R$ST+o?pHri>UhuIC#WL#Di2a~ToFL|EGpJ1>IREnJnD;UU%FD+?uM?8(P(qxH~46!YUm5tT=tmOGG6O{@kz`Scy)i}2CWTf-3o`}fZ1EGmV z4)*|oYtCTdgk(FP0zfyK9vnF@KqXxGKR>@DTV!Y9tS#wz;zo6WS`|nAvQMTNK8+FQ zV@^b@xeF>oy+x_=45=8o%aNx@rI(VB(%c6~fXEGlSORSoxzVr^<->jU+C=zAq4gYV zNf24GHIx{+Gh|bj%Pk(l1?^-duesQZy z@Wp+#H{@;j5yYx;@nzm3B0~}_%sE354loGu^9rcHv$xzs`EolArxH^H#z6lCUq8IV!m>bqLHTH?ORwTpAit{8Uc@b}HO95*g$uq4%O(H# zjxSg~fD8*KYTmS9vq|+UDgc~0@{UAilIqtmpyk~y>6eUK3*S>F$&TVm9S+$+DXr3~ zooo6-WP^~(f(ejA#s!l#Z)TmVOV!~YJA$`(c1i<%L2G2d(lzrtb!ar+N1%&q!M-5f zG11#c;A4De8VudWz=|$gBvFF%p%C7jVEGAvyLv@C80Kh#Y-SGj6f&lL*=+JA7J`Yz zs7u5!$(tPRAq!c0rPv0ca1NJ@KVV4p<^{p?&|AMD)u-ohU&dikG3mX$i1lItWQ!)>R4bXt}X6+lS>P86UAIwL5QZDe=$k{)0 zIzplWj_6cbfVzLya`xVMl`iM%c}k@d4=zP(h~~aw-#J%OZSzhQET1sjfREH(}Y;K%Wuy4(?QrUK^uZo;`04px(Pt? zxT9&ELQFpL_gm4NrqqKHc-6x&RvmsA95eHfO=2PB z{@>BSiYhip>Y-d__HqnhDJb?Bx3gh+!PKlp;xKSwPP)R-x9=g4_2Q5#S~zurH0zNk zv5!mdgX|Bm+K2qcRKpQ9P&(V{n^&b1zl|$yfexlk8Y+FD2%5>8JyZu_O@FL5pTVYf z+&Vq)HUj_ypueX0{EJ`ue-R}856UI~jX=Ix{vTNa(0@vqFhGH+|HYYq@elobvE(0v zQ2&uC_~-Ti&6MD;se-={1D1a)ll)Q3vl6iW4>V5fO#hM?{MWspe22GmVbPu@hE><4~ZJI}HBS=%_Oc&EWczJj#dE?#E7)izRNTg61 z6dBUv21p{use@#a;nmvZ&i$o*Xwk+Ego>D+9cx=Oen)JU{TRJJE~c9vIdEi6&C>Z5 zZwpCol%AY$YBdx_P=5|-nxVxwE)hv3ViH4Qm>LwdLB}YIlo`AFX(aoNh^Z-@W&HOn~!qFd^A#wh9Zi>7Gfig2A@o>>MDhN*9oQ$bDRp_}aK9-2u}9T+%wVkD zPG{pMp@e1-?RH;0{mc(=!-)1oIi!^L+r%77K!RiyCE+}q?AR87a~?4zOSN6ciI2aG zQM#`iK*f)b=$z=jEB#?X0pNmsywi&~3A&5NMom}tiT(dFULba}rR8PG2U6K`n$>}gG;wE>tBkpX^ZAx}LJXlImF z7)Ra0*+_3o_P&K!AgY8EQSmcO)AfM0&(E7B0GN;gg+j;5g43WhY6k;*fs^({6c@n; z;!mam9GIDgy8S-7c$A8jH$*p8%IMu)+A6&0LJ_DM0e7 zVu1DQqRkDxIn1!ZTjIJK@-s$+Y|{m%XVlz^b@=!Uy}nv@2}+1g8o~QBwb{|ws|bOA zQ5vv2vXd9)521j713i=JMCHh=GT}H@M%VsH5wf?9x;$7!ai6DA=lL4WfS!3x-^vh0eXhorrW=-JX5BRq%KXWh2dL|sGtiTOu#SXnKBvzX}n8Si45^+9*NMU~1;v^h|D9DW#2DYU%fwKYCK{;12 zdGHw}Y{|-8r(<4>U#wV8BN$M~U1dV7?eUcD$rKEan_oHdoLBm+(#Kp!gZFW&PuSGb$)%4A z;jipvS{>RzbraO02Yy!(9Z*-0qizvc06EGV+J*HZ+ULRmnMJ_qZH2?NMcdQp7k5Wz z4u_aNDwNXY8nyfirHs#wADT~4;U;@tSBFBb| zRYV+ne$ue@hI8q*r!6-RDK{V{(+cbBF{|<))y0c;}iv8i+k|Cwd;GF0H1AZ@zRurgPI6d?8waB=Zu(N8M)eYb*-LZnCPQg2x)$(PHWn6+ z{H<|7Wj+|8-D+f$WhBX@TF&AF+23>&}%1qb+jyQLAi5S1L2vaSIJS4PnlX`W)yfn+J>RXRVRPfJ?$-9tsNw z&e(t-D4p2XehMo>;5AJF0|fd~0CM3gVY5=M8#XbK?Hvi)KcZneLV@FqVd2{3FUSSK zG4LS5ZT$Cce=inIxEx zsX-rpoWPljvJM{nB7#^wIy?YI+=3=bG=dCepf52Y>CWCcf0wAh=;LLj4v48WN{ydH zX2uq}lW+}`hx`fQLJ|xmIQzwx-EjSu{vPkH*c16?+J_Vd`9@AjUd(heVhnjQ80)Jr(xh_5&jRewC40&rum}uWkaV5dsZBSww0$Ia+my}$>i|4EBM;_=tUgj}U1zc$ zxvJarbKlRC#ddwN5@dZeetuqH>AT3B7-5VPp>EC?`Kk!4x`daHdbG|KSfNiRyb%r; zpKPlxBCmRmOFsV8Fa9N+h5I!1KxlfY{DX^&OoVgBzJMQvPn`e?V;$8TJm@C}J*>LR zvEA%d5|E3Qz9<7#4m?D&e=|T1Rs-6VJK8{NNTPWxZt+iMUK5tnQ4`n@Fnq*jf;G)< zkky^$WZ23fNriE+saZ11GGKG?YOqHlJ_rX=%@i*1xQ2}sLYNz#g>WzbrZtJ-L*V{B zG$(Ir^jzcP#ZUKIEuBr&1KWd;%QJ6Swth4ez2dHF6dZhtsBVpZKLwkTg*Y#<)5ZQL z53Zu0%2%f^yX}@3N9WbsS^-G`?2kX5Xu?kf>zR7J%q4+1j2KJa>}$1c!Yjw6xb!?+ z8}?w7B)q-*gF^ky{>6_MH|fD3yXe%UpIw7bkT^uRo8aDlWY zs2u>P?=K-G9l&V=!myi}DXuL>x{w0$u%RE+*}YYh@?JxpnR^CVd(rDk zCNITs{gnvoIA0vDq>Zkw$0xPAX|YM4{^Lj2582+T=ppSH@Pod(jVPhp|33F z0g2TNW*o25s>qU=dK9JkegZuM@o@U3hN?&Ogcw;O6JHKZu1L)t*MTdg?%Km$6jUVQ zyakrwLvu?=ghD9;hKAgLc`7;p!9ctt^BX+t0fhfNUh9e_6{`t!rG)%ID4;JD>=L#o zdUo>gdw~)!@e9T>E6-(U_$iJZp-B*)o_XbKPa?MSc)D^%#dxXKBS~MDXdU{70|o{i zW|wkgN!wYi63=q3Aw~)0ph^nnSxJbaB9wmO7x7poOI}d3#xj*?p;FG^waeIVND=Gj z4`?v5KM;FP4`_0$dF6z%Z&_EV76ky&!``zY;1p*@$*6v=t5h0z-)nIIr$1PM=r;}( zTUdrHa3h~vQYy2cOuJTmOkIL2D}bgnUq_jF`8ifORSE@s3iLh~0`;%3=*4{Eg%j6miHL@mJQyqu+=p{fm- z8K*P~ZDLnS6_W*nAPd21X${h}(>&I#EygsBGqp-ZN1?10q%_vTj@mPaBa?Jbv(FLP zR|nBXJg@noyaHZUWVs`TMh zn5XKCc3zlg92DiBoudWdB>?Q=br8oO}eOIcdw163( z?`x^#<1j>ck__R^nVj8)+p<7pv-~F3qpm=^EmziY`dM<71L-h4*yLo#A0;_GC_|vU zi+$Rrx_iTOVg`CBbunK?kqe~;Ia~>DoS9c|(#{6bgEOzu4;L!%i%s=qL_2Ih|7iLD&vIZnSpV*h%=;^%O!Is0~a&SQ^Zyn z3q6%zyBG%J*NrbBWR?iF&5>{E><6G>7pxN2I`u6NOTHztXnd$M4V5KqCOKMD7y^ex zAX#&ldp0>N8|I}Cc9C0-6&P+H=}7bF zz|uCDf{F@UL%Ryg*~{y(%RH(4(qs}%O~uQ|M|*Q? zX?rz~B2a;PS=6jw(ORUeF7eQuwtyB|{ptN)=zhONU(IKgdO4lkS!LGed3?IPzixFA z+}Xk5eI7S`Q?sC(FRN}E(<447y zO!;FZlc#U7xh6Op)Usr*(tZ-PYt{sfDSkrh-gvQ5)vE>9${IvM`fhY4`RZyC)@7n3 z?9=Qmo+t&=hH?;(gNsd3#rAy{7;_it5CAo3PrSWfEP^oRcSgyd`#OB{&W3M)v$Auc zlPTTWBaj{T1@7MFD$*(k#U^dd$ds{+%aF~%7$Wn=!wZF$X6do!AxuQ%VEezcBz5A?&Jl#;5sTWy(R1HA?-Wz8rp}^Dxemn(8j=w=C^ImAK@p0 zV_fjPfey`5J)DZaPdP~A;)SW&lzw{AN{Llk#>VaqWm8X6hAz68^lA z${RxqEf&t50Kn{`OK=>giCCye3FUnoDK^IN$Ie|h6=;)BtZ|>ZyN3mI3APVD5bCG=u&_M`| z3o#}f<_;%*OrE&p8(NO@Oemq4O(IlMu^3xOt<;0C32FNsdR2&tH#2wE=-7M3ExlNCw;@wKQS+}w_~>O>-1Dz}C*Z{C~DK+koc1KbPps8=m;gKA7( z+Te`d#09HZ8)%U=PyaWL=7aM&4CJ68y8enr^A3m@`Qg$iAS4*zAYN!QA4H9+ zp&dMdc;yTx=%tKz)C@Plz4v=LYqOW!c~ z0r6fvyL(E3!Cog~#PeH9dH-TV$1|)N1zuD} zO!zD>MJIWs5vLRQ7Li>@=q%Vk)*bt^*Z%w%v+tnBC-xOh;TxyDT_A`bzc87QWiJPb z23%(^xx(5P&`Qm2r132fM}TU13*c=7|C7=wOw3h;7_7IhH`(C9{JB?*?*^#$ zDSC$2AVU=z8oholYLu{2Pt~ zKtq)P-}ICzjXTz}_`11um!=WF8E>y!jS2lV*BH?7mf?I()4au;nvGy*e?Ro^$zEoMd9JEy8~wz zShj^qfjnjMxwaAXzr9!`FM0s*wi1HT^(hnMZ#m@X3G7_U3 zc9>(V$i}h9@brcknaa2CH%kvvD?hw;hUDQw{QJO@IhJ*Uz^xV$1;uchjbqzIIcz<* zu0Xd$A+`qVvL?K&p%=oO`?#fNd0eW8#czm^f5!rOx)_A>%+ZNcXotzAuSRl2PI@9m z-gV2hh2`;Box9-<5#FDCHyMCK%AuV z4UD!ti|{D7Bmw9fj%l9#g(+`NB}7LIc;yCRIw5@eb6&##U6lld>Sq^PJS7EzLUoFpsA;Wpfn4w{!m$g%; z=uw>XiYf3THbG78%E6XX59dmaFu(QmNPB~!drk7_qo?g@Uc}X#K_&Jh%xC-4rdV!T zmzjhcqn^VQimL~EQZm5Z#S4!gduQMZ>h5ympC9#w=m8@J8LXL1RI16+l7-xNq$VO{ zwLHMN8AKXLtdI-H(AfF?)vesqsDfV{o`%#g3BP_K4zktWya7Zu#(wE=z`!}naqDr8 z4Vfqg(il=i11&UnFg>K2*v(G}^@vgURFOL>I9k1k0!@PyWKrh<8FJ|)rO2AEqr)6t z3&a(07UaH*7aE*m6(#7JlXp$GqR-uUFZ&#vDd;e^4J&V$e=H z?v$39*CQZ!*Qkd$>)RmtTzQ=xY2 z<{?+=IWrf~1Qoex23#^ybd0~Z!0c@fQAkq{N5+Nv^S7nW{+a}fHoG(MFYxra_1!kAR-|=`pqwwkA32v?)5Q5 zkHKey=;u++aB8?e1(Omp|{&x~%OCWBaatjZ97=NtK} z4#GX!<4b#ghI7m;^`x%DD(kOd!p-pW#UR{~l&%Vs+AO^dHGzli0#9>`?Lcv|(IhXQ zu7K+ALyOb9Z8m-CwPk5txB5jo}Ie2Q3N8A94(CB z^_^OLH0o&+t2l-><$^!|4UX#;1obgD%eao_@ySc#+tMXFA>=0shp_ ze#isEVm~NDJNgW`6`pmvX`0{SjC zlkPfXkzTcd#GanJ$9kq!(Wcge9R5ntsf;O+;MwV3yomU?=6Hb78Xyn@J@F!bdRf!AlODIo0Z6wc}6| zMZ?WMTV!R}b=p^%O?99U2b^GA5@&c)=E0-+doB+e#$UDou~ z39tGV#MwE4xJMN*qfQu6`mLzrJ|p!JA|j)@Uf{aON}tT8^tVsDo^T zN9ne*oKZo>u7!8-S1pu#W&{ZvawlCE37TH1E4~FnzTpk~i;OQ%)gs>SU~1E%jJ4@aZIE>|V9muRZ>Gfh>dTp){TcJRZ{qJyY*H6`eNc$S z!Yl*o<#5Ol-humCR!?9_QvD=xWPSPFj#d^@-UL*EATO6oP_;PTiB=Z;p&%pQ;4JMT zRcGEFSEmqr+X$Guk*B1R_GkvL7=XyOd-DsJj(N;u$^0zOdA5&^s+2LB0`Kalr^_BxkkJq#46tSz6r_G}ahw*TmYKZZ;F>O+y6NvVqeZV^*fK5B--pl%e z2UM;hP=wl_XNWn6_j@3wLvZ>1YcqkG;g|eabFAGmKQ$A2BreNeT6RV95+f}lB%R9& z+U$NvRyq7!IxT6$+Gn9h!qsFK_MK;1i@03WZ6f85@&mHQOnN}9!(3fxf<=!Vi}6VS zC$nlnAeO)BSb2ND7@UU(sIYZulIdL=mC*RY-mf1ogz>OM1U)76U=q4 ze@tbW9~naS4V)u#HftnhMDaFbKy+2`zhyvO5l}>;fI^@3GqL${z#t{7#XMSJ3^DPJ zKqBLxlk|cg=u@Bn(DA70UoAeGBjJDv?Ep{&SBEB&E*>RsHxw*0W>0nDU0=NZfe)L| z*i1w;jVp}c${z2-&)3X>l9$jOHQj!8ZiQH{5M%IlT(_DujAo=TVsV9Yi6{hszU=KYWbpiJ@-bX4DM-)^yzf!exC=R%TZgvzQC% z7#fQ4LW78^n3PrTrcRjlNMx)7_(&C1HGq zhPetN@}XmdX_}qQu@UjlzI^ezNod81_RoXX{;T111JRBPP-(^|tunF_JC%w2$%dIG zn^cTLY9qif|LO+UvLeDVv=#Wm!fcTpwUt*K3=|pcU+$OC@=GQw<(w<(B8wdy@Cpf3 zLH6*S0d67jF-qd+TwlxP%D}tS<7)EP!Hgd>*1t8vSO)h4jJVFijFteciKua zw9gf8cor4Md7lCEw+yKgi>NoX;hf*5Cl^yoqknOLdv@nz(?>&33_s!l+8ayGNGtYz zB+1PTkObG)b`kB?=q@xT!PI9M6W-QLEEyyIuxQ8e4Q~Av>gtk|(V*&^A0%{aCcx({ zJ_KT`G_nDQaHud4q`*2a8@DmVG}+m@i&!i1Gt@N{o_t{v9QC@>ThUHt{ElxF-1a6T z4_?#DJK*VQw({VaV!I?&cWxZNr_=NI(NxEdF>~u2an9YjleXh}<+@&tQls+W&v0;t z9I^}|1KxorJ6k*@AN6aPI!OjEc?MxZ7o^RLK*B-yqiLO_5CoXPS{fp7Ld|k*Io=|qfc;WS=@Zv@o3RR=jT}{g zG(3S7lV)Ii?Cq$Tt1Ii>V&GQ0+V#uxe;nK=SYRdM9mNo1dh0*nc@$0vwRp%>=Vo#D>yIY0LH;+4#oobVki;BI#Ng+=Yk z(_qfoA8?dn2V=p>MG(j|3y-%(rg{8ga?}^txR=UOK0!BrXA{YyT`s1vnp&XIio@q% z5esHkfglN&AqvRJ5F^o#DIe!m z@_4u)kbrBkh}ItnOGq_7jRXD*6oww!$0I;8a%tlV{?HJC7-&0?R&VD!0zBCo?-&B*M0&`$qQ0AMOPhls0(zTjjaW&oVSK+aW7)6A}5k>YWH3WkRP zUvRyPGm|h@kc{s3zJ^<<_w2%bpS(HuK$1=JZ)adhzjtjM4#TkPz7I_O0LwoqWJYXEJ~iRN?hX)RZwUHP#%DExSO&yWdN+XVuCd6fCB= z-+`1sBz^jc7P%&07!4}6MFGOeoS8In(Zw8U*q8YZ=PFyk(r{~_9=pI^6eTI7Ya8BI z?|^NHdM@=6wkYqBw0w4bu4~u_iv7~H7PKHTfq*ow;nbj2)T?4mPrL>~7)&;W-#2&& z5JjbwNsS@QJp*LF!328w3sa<8IQp6da?@VyZ>zj%xQ;UC~8BhWwB zH1mP~Mm)v&m+I;NsCWL;0RGQboRcd%Bs06Oc^DvPgQlQ1s0>m}e$=Iz5{Cw|D5& z#h>?whN=!6f3;VwTpkbClW*h!uEBvznlO}XN-d74AhaV^3 zk*V8Rm3HAZLx=Y0+WhI#ql=r{^DU<7$vtKVZe(p+Q5UY~#qC zBs3mGt7a2RJbcSCdFS)LTx*S2H%MNt)2S7)##IF={HK{mzUDHW+vraDq_}V zw1ywuGfgs{L=pA7#x;TVn|UI%nZl($*D(xP>2$LRxg>A&$RIqKYg?wMZMy*bn=gyH zd(+%`O5Gi}OABQjF4WZwjl7=)i!cRi{A_|j2$LD1vTzdVB}?uGw#TYhRiD|o*Bs7A z%^~oc=r`)8aw*ib!_m2FDQZtU-YH!urHVf8pu!u5(WMYhN?Z~0{5%vagGDJop7G}A zSt~gKfX5N&C9@FMpZsGwP+4l~`RVexY;EHOASW6!U5{3hSTi)h9PERC(*|CcTAwu3;1_*U=ZUP^cwW9}7<5>G zwnuXR=PbXmitY$f*3n#Kwd|J!@fShL(`Y$+^8v+Ixsu5*{9_tFLpAab+d^Yr5VV3uD?KtSSo*nJSkBR(rb-#CB_~*C_ygs`LG5FNx#Y ztLwY5db$#hF2Jqz6c`6jj%NoUo?<<1!mWjPh3@ z$tSv#lm-rtJQ-s)cy^ax=(nVwdpF0=7DPFC)!gL1GPos3ufE=>vd{r6r2Ugxf|s5M zjjXm)bM(G`_3>3RY96ER*oa*W$IOrd5dhksIRiIT7Jv{Fyn9ZyUg!bkl;Ra!Aq#6> z)eC2`@H_Ry6~mZb1Jww-=EIoQqq(RLZ1G}aby8hHui#3C(E)6_Dt-;`rcfcYF-w$K ze0ds}@j=d0B&!Q2e_b_b@{1O$)-QD}|Ax~waF7HwA)lc>+$XpL<)PS=-eJ4H{qtXY z%Xbp|wjGl4!(HTJIgO7AP7bYlR18G+4ObkB$g7%tXEnV407A71c#6w4`{_eo=xF;1$m z2;a&^7JfD|pr0T1bM+V2Y8n=gtE&s+k?Fn|>RMQ>eDfo_B5hXuYlxMu@p^Do2iZDgV7j6ssUhY{F*~@*+H(ZQo+9+*^NaXk&@oY$5nppc7go z(tw-;y31yXX=@~_mwYvCQw}81qArOO7FyXx^=eA0Z~JjYKs}K<6)jC4w#r91h+&i9 zmQf3z9YQL0OOcpsCw5{RNm$OyuXD%!dX7Hf;U;+4PVc1ng(NT^{MA!Xn9i7^ThY{Q zlJOHN{@@ZTC~2tgV4gtgvW$D%sipp+)NGef$u4_0jG;`e&wA6y zr@E>~lNYrCVK{=ezA0fc0I?V z6_*i@6Ko+sgV_N9B{T6^g13%#Q?#Z7Jb#B{1`A5zG_108))a@CPQU6R;lDjKgzo+OnUXbLbGU+mXmjeGwPiwb*O(DsMdfi{Aw5=|$x@3L zmv4b&Wj|r9ubhI(_>_38Whj?hpT!r|1$|iy@w|KyKTJi_sTex^bj(!@O3I_;2AYDS z2m3??S0C102t%o7Dxxtm^8zFWsQXLl{DiBU0*=8{gGZmg5W`{H{jxN^a$X>$Vt$TZ zQX2TK(zfQDbv*{EfXZtqLzq~uN}PeaY<)jo5|RDVA0%1%QvIXr&E~iG9m+-_s-`aa#o);{mD)cx2hV1``UHu?V?>Q`$Ne99U8(I_rIWL8MSb+ew!*hBYSZpb-m=eH8%EXtLG)m=RC>_X$}qz zSW=bp-SJUEk~#79Mu)qg*t~bx3*}nqniV4;$G=3?4+`=_*=$t!)}biecw9|6p7J-0 zwrA_k2!+_l2ZL;azXgApeJ2wlTnc|{g3Una=~#IB>|^AkcBr^1e+*R)PnSS!MPetG zt(zweh^}w0jad`h+Ju<=bNX(E9Gh2@xMS(Z>g!l6S@wmIa%m8@*5Cq9udl_$qLcN^ zA=8$pRdd(!;(Z;=iQVfkBk7U&M55C{ShNvSyxi5yo&dH`JfVb$6RT0GHxWAlsqX&-~64Bd!{i!vFl zhD(RUJ~HL5Egq}W#%`2osrySv?^qIkO=7SLj{|?8?+*^bW5s=5Nh6DKkkh&v8p%&M zDX!Y0W~Ba@V%B{~z&QZ_{yolUoMM?jZn9|j=FEichl2@=FXz=>E1zCel6rLH14kdX zb3sYCu{yGuH_AD$s~vU(&n|vE0=&rYhqJ4IBOeMz6(nL#DF^%&LtIPd zYgC|%b-%GsH+@RQq!QVnbnpW>{~7d}Wqu8gcVAG-%0cDCa#pDXiRxn~UJd7`7G@A? ziz|hQV*^t^iWceedgk;ziVi8-M*rwMh+fHZtTCWOQa}nE=0jiEx)B_{CH-N@Dp(3q z;|65K6E$J3>XVn42CEMpFV1OCaNRnTVF=wwx5kqO%M}Q?t7|L~w5@!(fXb^qOX{_u zFO2Bw=_vHX4=fbSm|=xO$x7NN9vt~HBZ_`8aX5~~Ky$@EY)NZ4x(a`RzoU6oy{-kF zx9m4&v_6Uq8W#&_lAR^mIz~pFVr>x(*NFM(;KTs&sEO@5ulrNcTQA$^3<#p&iuzk7 z3(~wv2>B;1?T4)iHVCo?kSvTB95yTaM144qv<#kU`UGU==8?#Si*81$5rlB**Dct5 z5~CyTum*+AvX0peG-^TCh^uD@IV>s%56&MIiNIN=?F?_7?cl0yUlK4tuOB}OuUU)u z$UBbzoUj<4eqhq;8shiOO516OcdX|zqvK(iIL@zSX!*64w0y>%Du^at?M|(gq^r*E4S%dygeEsPVJ>OT zZKPMvOevLJ$63z&vvnf>?VtAWnH@J3@0w znZing~W@-m+Oh0ER;-JhQU8Osq>c(9K+opul#_;{k%<)25T88*Rb-K=%oRWfCPag zL3a|N5*Mlo0euRc#-F=S6~aUG8wiQ@9p0Wzen3rgDJJAaGKM!P(hZWlT_6$hX_N)qDOt2f_EFMnO0zosGwaeF5MD z8fMl{20%i+}K zRuPcLR;nnaqBKSv);Bz;X-uIAz2@X(H!j5u()J)jBKu)FXnNjE-20<}J!5MXv7#(% zCCSwOfk;n|%^t+u$<(1tyiwbmY%Qfi4L7}IPuN`dhK?`eV~}^QKasy4Qwzots7v^_noPoS8tH9t9dbcNy+ z-d4e|tG^noNm>0$1|Ntw0 zB-kpm>2#JO82As>jCc94dC;)(W&q?X^&fL+>r~_xbDRpkGQ|8z{qO|oYrLtPO7k|? zkEN1oGek;`fKjwrOnn`CQ6W#*ksiS5 z<$YCCMd}XTlZq79BQVQ0+Q!JV4RhxS*ILBj10x^{TXpa%xFWGZ_EfVz_C!qp8Ha zhhj@FC*1{joG7JVrJl`)*V zGmXe!AGkfde}(eKz+NGU?!HKG?g3qVb`1atd}U<2@EG zSGlfbV;p@MfYD`(tG5zOvN>b8C;w1WZ-tokf)i|sIJ}-)p|az)to!F80E!Q-~D^9hpqhlc{AS%uUm|4dC$!E zPs@&ybbz|D39B?l4(s9t7-xQ_P)mwbJU%N!K}+j$5}gtMOPmR%)QZxYIC ztF^J*A5J^rc{u0hA}}md1m0Xq?DcAE>fNhJ+Z)0>T-f5Rz7LTzz=VSYWT7St+n5?Q zz7ZjiN8{XVd{rs9o<`({El@=Udl5BfC`~a6m=XIAGNDiwhvzfaV!iN$;ZU;FkoeA+ zi67jKdzY~SvM%C7lXlgEv@2NC5I?vJnfpXM?UhUh1{Maw3ll?;dWkhrPuE?JB^W zHA{uQf<6Z6(^>T%q1{mJz{$z<={7bebixaL+Lu7ki}n=2XzmxvVSlrL3HO8(7O4-UluRsp)RTT8qUT+3P*7O}-dS3$3eJBw zS^iHrDgNK1jsJd{@gE7s|3!s^^KUC0|03}Co9yX7%o7|8|7xBnR@byAW=HyCp4cVk zj;3`WzOX_BMwtJ>-1 z&U3|fbU*0#x_7>Hrb5?xf!fibwFv$^vh?KoMcy&tY=|<^gt*R7R_#FLO5>an-#%)% zQFijNpdZfK@$RWxvn=k`oUh+{G*Ck&-_dTw|LyVh9X3^i-dL3-pGcecr@mpSIaPxO zJ%*xfW2PjL(J+)abvITwq^=f06dE(nkCJH0W*j}ThWlH6|COu$Gu`4l8O0~fq_YO8 zgj1P_QK$YeZm5 z{wAHi03z?>5sB}Y{ zEF&G7x!lA|y|X%yw9#r4;X{8UmEedvZ05@v2`32EGK~jbEpK+ud&IrRRzSroPiHFp za1K`4v$x=8f&oxqhA0gb?G#@XHS+WYF7>n9D|}$E#mbg3Vr<=PaulitJkvS1b@-9y z0p0W8U1C3|DMk4(CCPQd3@5W^K4h8LJNW>3NPkn$A8P?f`N`bW>!^aw$|DKY6R0^CjJeQw*olPZR(KemPF1_1`V;)TwGf8fHd?im-! z8_EiGy|O0D@)sr2zt$mz2oNM~x;lb~hc2BKjEh;h;N^yG<}-&gx+5VAzyMAkb@~8| z)&iqvtCxVWh+UD9i_#i;I6K(mtr3oZ1~{A>tu5`QA7R%TZkJoIP!K+p{-vlq7Ph+g zv_oJQFH}u7O{vnWS70T`^jp?|!ngPHgl7V>5e$Mv0!ne^kX_Sg{DurjMXQNS?E{H4 zx!$d?&p|SaFUWN`S)-M0JT;9he5SAp$3H?Y?L8<6x?H*KkwQT8<&{J6jJq8kCzj<_ z6Xr40j)0vTv_(ci4Hi;p$f1}fo;p8U;L62=E*?}O_dbOL&fqFg!XI@X!%25*7W3~h z5WEDqoM7b!y{%ojL>$8?Dy}ms2x&2G^qv}z8iFu$!2l{}45+KGRICehI!57zNVqcT zj0i(Bv4G+wCp#Ly*tj{Z@+qW8z$zX@+cX)7qOpEoIsuskfX4xsM&&1^B1T8u>FtNx zR)0N!+%#t8Z2%brPRoClL0OgojtKG^p`>7Kafay29Ryoh+k-&57{KBL6ZU|r>ib-_ zAlT!fK(cibU!skFZ%HoME1nhCPST?IAx$)5uos}uLj{J%>S<#+@C0=ZhpBAbb^^L{ zBMx~GyQ5t0E2SfW(8U#0nyDlJ{Zu-ZSId?o#)C2%566`_7$1{QkHrhS4u-;G`-LwI zzvzRN`2{q0aOc3GBn|c<)+_Ah52+9kcKG_hrO@uY7G2;iD}yz?+BJT9^54D!U|KB`qhyV+nUGyjcnFBEWrRXmUsF=+|oIaOP2#h zcA%iZff9}Y*Dp>(y~mX(Usf{)vB^k|+2>l?QLk4^))BWjrGQkP$QKvL_wq=(#vk?- z=fAuzqk^b+e{WVw04cAxeb5JwF;EZ3C5yMmoriJMcUTDd`|AOj>>&^PQ%CwUd25Dx zMk>;M$UErc<+0ht@In){&y^t^@O$k$0P>+~(cA;C0dcjCZsDOP^+V3X&?A9< zT*WiwE&pWuNf9QNGG0Z0M-k#hHvCTsSzevF<4jCZ6Y&^+C1$Ir1p$;uq0?8gcmr9I z_3H!{GQ9Ke%6i0pMifm}7_zs;5|y#i^9xqUC3NP(H5q5GX!Vxr!_WOs~Y zQ&(5xj^^dTur8p#06YA=gAvWKm??Lu7As-s=%*P9v4)9#HTPx`q)I+lT3_vwku=wx zzkNrujwgr{*8mS3pqy27pFaJwc+JO^gIAKq7<{;rRk0&GV?s)o$Frv#(+f1_!6~+U zUiPi9(g@7ALVu?7Ab&nq_03GTQ_fcup{Ew+l%lQ-(ycSsiz$&What>cDX?MDutnZJ+}^O1}|+Rvi~M?lw{tz8W5% zzlbt*D6P%-m^Pb>h5QwBx4gICv@S@=xBKPhjz!(QVri?20ZDmf(Wc_U#ZAv85Ry$n z-NT}!hHS3&9-m~&S~87&kn_TdpcBV6k|uHdI?pgB1_vc>1|-*0?GT_}4e!hua-207 z+HX0OgF5|>7hg9UgKvM{kVS>t1QvrDx$uJQe<*dLLxFQiCHisboh@WASUfQjnRn+7BJxQSP9f5idI`O z0OV68pdW-AsLX0SB{2nJi(3rC+6~rIU}cj~$gpE);ZY{&#~n1Qeu2xDtu{g$q2&&z zIBFjR^h$Io1JBYK?WQmxS}V1^0<`UuvtUWJ9-@EI#^eA%HlrC8t!~D83N@C?yc2m zbaz z1xvS3L;C}J5Dj;Gl%3^rb>RLCKDQ_Aw-1;z`~1R&;K^dx7TQy{>{V7e+Itx{n3@|i ze~CO>WY;zDa14`?9Do0S;u>zMlnh1($`)xaA~%4-ty<HM`353why|&2(H0D zCF`cQgqPG#Ajks(UB=vk_1T#ke*dlfACnjaPD%k9$3#|^ioM}rSKx=oNmU$?3gS>U zCD2jd-ki+E6yr#SCjy*AtRy>=S)NG{xRBEYnmNJ*)Y&l^u%E@F#MmnJu*q^Majw9s zwg*M^X(Iv#_5rFdFc5%PwXKB`qs`38>-|2lNMdCEb|sL9FG+7!Gj5G4Ov9q{U51YX zd}5C%&mCxKMZb7~@myfgZu)S=&@xcXawC=!EYVA#*dZ%8vei2ttV|Rv$E(~zDhkT4 z3cp;3FwI>FFoc z=MOhwO8a6K23=C=l~hv>%Dc87+%H#b<#=rF8f$nB{pJVXiQCn)Oh841NKl+uvk9Vz zmLk;C^9}H9dLWg#Xs#!y|K!!HtN#e5A#0V5k_iQF@cF9X3Z1Tk%OeP#qxFlM6raa?!8560n zC4q=7o6Mk_f?XCbZynbcg1>S?kTA25dp#W>D2dg_r)v+hwlX@7#?^F(FOM7?r_=^? zL)4M9Il4GM>jmc=0umZ$F3n(jN5IQQrWUCkThm zFf&wBQZ+*+572*~hSm&{A|B9AL^Ldj(o~K0qkd-mJSI04SvzAowf-~C(pok;KYxB! zJIpVJ^u^+e(=2BVwFlMQoWQJh$nr`+8|=3^&;JTIhNU%1u1B}PgimxGOZAD`-q4cS zAqu+f%3s>d(|N|$r~LS{walHBy4bQ8l66|dTFNo`jN%}h%ynjmRyw+F*A{sNP2;F9 z?0Q5Jxs61xy3Y+MCFaJ&jo1WC)n%K_^M=%k3NZy(g!Wgibdg@R2v6anUqcYKTeh(! z$Za=t{}CcSg7j3jm)?NVkL+Vq2PR0Hk{P7*F=bUn4HH`?<-*5EXos~`HJE^d*VY!_ z%7L{4hzL6|qk1xtH;F++cTUJ86wP!rE6x2FL7Bsh!4EQC=J4F;#*_-L%f09~qsoL<0WMgLhPgaPZf2W{|q1TF7ut-aR0 z^IV$X)&g~qH^H3{VuunYL{kow^M6Spo=oX*l(tELMA*#RtE%L4xKw@L%vyIg)hBqp zpU&^6{Pby8o}#w&=+HRj|^;mf3%pT z^(-z`Zr4wMezjNNvB@^?G{Jg*y`P`_fgrUe>1z3rYIXJ9PdHg5>`LLH?jk z{dWkGavX+Cik*UV!gd->zES&|QqnU=qg$HAUrwr9(rKG5az2|f0i#Ca{$n4XIr5n# z^Uq0PI&rAStfCGrhE9!n`Zf@Si}hRZ8n4)iMOX@#cfq2j^)$6d{y0j#gzAz=SD2UE*R5GykT4(CIA;8ti>zAKSctrZ$P2AQ}*b2ASR(hh{(1ErhYR zXbaH{0_1%q#wvk-2E<+6Wwy*53D!&D5Lhr{*E{tI2Nb|CGd%ckmux@Zi^BsaWzd8~n5AD>c)v5=u+2ETMBcu9 zAWV6~BtLcC@B_@yR6cT?5D=wIP(VyD_UG*xI*wg_HZ8K~0#~n;p@|<#prMDoAuSnZ zG4XQ$d)xq6H^Yhm?oA(#4Fp@2czWpXwzF)vlqM(5#;)2T*Ds#Z0JH`bdXL^JGuBCb z&`fK+#|_S)T>2abO}`U-*e-@b8E}!#Bkx{oSp!?dZr0H}!4gX1MAT1yGTcvRRz!eo zg*j@bS-e@gd@&HPpQ8zc{6-#x&P0NzK9(H%QW!D{#InS!&%3-H6Sb75tvz+c3=J_9#VZRzgv)65Vf#wP#IPy36OkqMR zJR=;V2op!f4ulUi&e^bua1{2>EGbz}iWdzauSmQty1X-T*z9LA)Vt%f4A!)`$3ow; zj)gxnsUmfrCaup-T5NA6g$wr_9@`HGy%%5Q9iWj`lL-SMXE>ekNg>@Q15ookjUqIg z%_F$?!{58Nmtf1?Ijpc^*Xsf4kb#6tLeFWx+CZbiR5dkN1=+tjIZNzX<*Eh#NS*NT z$vd!gO8Y%i(7(9FOC_G0#NBXP^2y{YR$FIBHK4NNu>33Y*0){^NpCy}xa43g57JJ` zIFrPufA9!QkFLhctX}J(l}(`o59%*wHZ2w&REWA2)X>zYCMEv4wb?|gcKfcXieQW7 z)i(svs7{#1&=AEqEkgBYDXMjFhYUAuC&e}|)+^;He7Nn$+F?$v*`9W3JI@n@WuhSm zyN{wZXk2xD*fQ5Vn{$Qv(ao+jFgx>cjvpK{{F1P>px&Kd9kPlipmJM_heL5nfCF(8 z2*jY(dxqdZToMW^$KzTf#k9+&J5W|hVoNrt?4?=c`rMh@Q}%O*Sc)4z0vpOld zEr<3T-!>S|`y$RSuyMqn(7TkMfDq%WM_$5&Eqr@$-0c-ICDj8pnz*bwz5&0KPJ|Q8 zI0_Z=KWbL_-r-u*qam%tnRg6Zra(84_PpbV^}@?LNGye{!&6S~dADMe(xNVigXEbA z>u?Z<3=}(clRHuhoJ&M5l^9rX{N8joz@`PD@g{b;M)p`(^AIZ60H#z)^~7 z{ph}RxK#}%SbdH8^@yC%xGv4_k!*~%2rck+AW4N^@$K`#fBKNnpr4467LF~wqP;o^ zvTJEv*awcqp}BOb?MhO03NH4ZZK|&b8rI@Pjy;DW!Mmc&XYLYeID(P|4|DXnNK-!z zj+=gf##i4h2Y&%{GJeaps`{%lH9n{-emT(tkUo*z7QGU4wV+m+syjI7A-c74Qi9}i-bhy&Hj*6Haq&oURdt4)I9 z;;}iY;6tcK4O)l+@L`@P7~UfC&u3R;F?m-1H?i)$4SFbveeiFkEPg6I#)x zE-FWRi|v*Hcr{Lrf%Pk?v9@OzC95$F){OL zXXKZi@$4s`j9hE|e@l<_dX5mBkv53#lOx<g7RQ9VylpScYt&%&>J5IkdsD)R#s0^9^<7-`Ios1~YHjfCUCf@g2Fm1fw@MO_pB zsvcF#DvygAKaY|uJe%4ZPvTzg{W z>(YPG*?0lnrn%t<#ZxJ2K9pylhSQspKp~W(g#55C0w$4C2{Fqpo`?bitOpIurgYV` z^~}knQJ%c?z`&(O_j}PP1Y(y}a~Bd&!Bf|jf3QL7xtfL-Nh`#q_?b=y>*d)FAD2zQ zd~zl`l7A~oy;Ei0t?F(Ouu~O*E6Iip>##g1V4Qn(9d8a%5hLw_4am+qH;gQKr3n~a z094Hu)`U8$Nrkn!(N5y2#Uz06d4PDKmw#xDXhng-BaRPTE3@bV>Q8e7u(cx9SE``R z+yvSo|HgL>{6-Us_f^eIfr^{6NO*o>E~UU6nyBCWA)D-M2pAQ6lQm=Q)`MnELQ8ko zFJfvXlI1hlS9>j!&I;tCp|qDS>Y%AMh+^j)!Asz3mwpZ(lMFk(?wsV{amR??XQO_6 zXK(dlKLvyK$BNr#6fj#NXZAzteT7;{QPMvAp*3DNQ0{=DKVKDUDq-_at{puIWni|Z zX@}tn*(lq}6UT5WcRN}*;4zMAPD%uQn=Rvmx9yUdSQugnAq4qm$^pa`2x)tYhUIIB z$}kt$1F}b-OzX!s(y4ayOcpqO=^%z%bBzdWksqOidjfX&$D{}XO|-0mTX!_|zDjKb z*#--m3g{#=<|JAB>=N}D*<58A_59}3i*rV5GD>Hf$=f?B;ADNK4voEr?R|?bjf3ZQ z{w;*Zp^aMVFxbm}udf$4YHFCB%aUHYO(=&7Q84EI^N>Y82{Z{%^YXn(x zcmNxd!WFSOOzBxXPV4Y~i8{`{{eKs<~Sh=7Qa+0DwHQ?99oPJHt*drSkNU_Bps8UW7b7ZQdGhqC4D1&L1L=Sbh8 zVUGwT2J#Eda1X?FErt1*b1EmyVG%zA6ulp@$zu3%uYJTfGzAR862>Qr>^!-hCc15|Isp)87;7$7%Xv{csO#LoRn{Y-;%>SAFj{XurUhz6B( zkI>w@YlPr3teI&bzc}xBb|0epN}Gk>Zyf~BS~T;#ZhrozNJ#TWtg;x1{ikRYc?JG_ z1PabQ8hK#mjyav)ox*W%cy4$m!DdbLNC8}?sh!H*9o_d>6Zk+Aiol3z3hX@+=+J>d z2KY~YFmAtIOCTT8?aD88Si)OF;k(q;zZfS4dGSB@y+p?p6m^&C`)hs{yoj4N)ul!4 z1l`Ct7L}jtHH)~dTqJi4f<#53#0?*)%2kq|l~owa1V*)ivzP)3r)fq-I{aU|O6!8{ z9ngCq-H9GXrV7WN?u({bQF1T^5Lw=v=+L@0_OZR!ckkq%Q4uc%*Q;Z7FGXBMbq}y< zx`TYw!Bws(NzU_+Fcmu4Pg9=n;e7fbBTht}0(2aj6tm+p%pVy&!?q^uaftxWnQSw? zyjw>_NqHHbuWPT4TD9eOvlcQvMM)kD=Y|slg&SvqR^{Ji!elUx!m<~3$^-y5E`5a= z0o@iWEa+zkI7lfEGouuXs_-k*#XZko(V)++~GOd*(PzyKer zd89K)9}hi5zTjE;2;qUfpMY2q%<(EbbZStM6<8wS0tj4}0gsD~h{4lHd2>!xUkZWc zwC$3RA{v}jmk@Of!25<*+GX>0X%o+e*_FmkUO&yR)(p? zG1i^MhYCq~mQn#GNA`Yr!X^PATc)xPV18pkqYgwq@GxL_4Ep8 zIG#rrXvs!g%>VkG8%KAqG#d&SQQ~IkxcKUlcC*WmYNe7aI*)iGz{R-sztrNgmjtNW zlhk+e+^&&$mA=AoGj%itv#2kz-$R|DC3%+XLpF~>KwPk?6GqHs?8&!B6kD2klDEJT zn0p`5;@Ug%Ili@}nW0UIh4zC___ImIHfXEansoYkJVRS0x-@{(snv$kn?w{%fGSC_ zu7AvyzPkUwp?uPh+PixRwzXr6ShLKIT0ckYwl$RBh`r)i(WPy-r9S3)FN?g-&*g2J znr2lJOaR8v@Z3w^(&%pnZlc5JX|J^~vMSiN7q&DR9jiCV)o4sC(nOKSK|I{*I^(o=)A8#1#g>+37%Wm>KUNCXZ+zNWb7o=Avh9WbT zddt0qviQ+3T&i$%_E6|s5(kQm!o+AzSnK>4xjDu6LRC1+2sj1Ps(jPrZ#a>A15>dU zt-i=pf(|Gowr(bxyOu|G@Zf?@+SHZMwImNH&)fH~=r9f8$6z%o&{}V&RKeLZ{iWs4 zP_|G=bbDZ?1b7#^f*BI90qX?KmDkOgH?~7=!22Cl?Mogdn_}&fH>8#L#|i4}rN0J& z-_AF#1=gDCf~IDd+=2nA?eQ*tXs#_*jIXD~Kf{mpU?9h3h#Q$1D5N-aU325u>hG?= zwjn0)J3FDT`_cC8keNwKoyGTWW|y_|X{lyZB$_(uNkox30v>xsP~2ZjWP+u|yneVt zF)0IrOj{G}@O_J^5JQmrjagsj!^9PlB(y)<)O8MY!x|i=n;4LymytiLb$Nq$*$W}cH&7j z5tqH}KmzAfcm*N0X4zgi9*BGhKSPkW*OG)9AS|HV<=jNwB7W})+aX8(F3JTTzkp~2 z=CK25pSK_2z&o`+ZWT4jq$XzDhjxijdj{hL0CTn#-_nwa-y`en3Q&2*&ptkz!3BHw zPYK6B*o8RZCeJWq=9Ces1o5!wAvr9;2RaV5Ip|o0sIjaTc>k_Va6e@rF+D4EVv+%k{a2u5`!PhTEaXCs151O#X)M zC9VpEKM-2Dk0}Q$nO$|f6AVxqQkKqB_=I65e$Ee?rKNWpHAsn|bDWXPW(b*xbz3-? z7Ybwq!~;wQIAvm|G#a*)fW7kKl#uL#a|Q}acXONSPZW8CRzY^hFq-z+uo{&I&_IH1 zeu0^Uvj@uH&DUm#63>q)#insu63t5}f3Md6Tz^c&S`+81$Jwd8LLerqU z(Z(KQ!5z%8L?f3Pr3Ydbo@fX1ra@;D;o(AS*Uyv!C$p_YeZ+3JR-E&9^A?ZeI%<$k zAGa>^F2>WOCI5S(NMW=PpBT;p2KT;6T?70lJGpFt-?u&&>vH{uNX z@sR5N{PJ{YAELBssJUG{B)O~f&^%A9CDzrSvvp9H*budpAhfOKq;HL_rxi)^)61_} z$G=p)_YP*H8@^j#=NBB4wh|2VpZcyPqRR;N*Mfh`Zr0#PDf)o_Q?05Z+Z z^oJTK&YU1OcCo0_=?0`7uSz%dwugmz_u*LHQUu=xsNe)2Zy==wP{Ocb+>oik3*qD-3Ti zyz~tHOv51qY2<*KHQa8*}#diDHGaLOH=eKt$A){)Kj!mOu6oSXRD=tIOC!=;MS%MEHp4=$jDd8DMlMw zjJ%IKc32pJF}~RP8bQs*qIbCr19DikdIN3CaafEw3W+K@ws`PN+p+K0c+#>Bt*ih>Iii0r50^KxRCcoY&v>Qz7E2S8(!X)c>%f!* zVGfHS6xNWW0ty3+fI;AZk_#Zq{?-2fu&Vxj|NpSB{>i)ekJKwO)4#v*{|)=<|GH!K zAI=ope_?feJ68XU!~XA0s((o?|L2wXUmw@M=UxF!%$)yuFcM*v>NdaDhmb#~^c@mB zxlAVJFFJu!+p@L7*IQWSR5N7xN74{Pg;2D^3?#-FPwu;2pAbSgP>`}#Sc{_v4E1xo zT%OJkT)NxR7$47H?*62_eRw*k)2%M9R&LbhOixXZ%b(MX2SOy%&?lOyVH@^pCai-|*xY;gyel^N$&YqtT4(x`_3OgXyIpJgslstmuY>dJ(_eP#XTqNd*LZpt8I~sP zO3I&q(oA~HI8dnzovfP-lz&*Tzl}J?ad%Ml6Q|Gts55wFs-NY<4n&Qf*%dug_C1pd zJ9H>>IYHyh5$IliKQ4!n{f-y8d6H^1dAtfkABvq>b?n3b5Lip>4uQ;VSE8!uPomAH zQ0`yNFoE&xk}VErymZfRoGH7Mw&rnwBtc)L26lH;(GJGQeG|${PJ`$~kV*WlX~f1y zp=GSW0vEC8FZ5S<|Jep4n$_L$g+;N4ar@D!M2OqGqMuDr0=0;;7)Gd^g*eLRFQOWC zvL$6@!Nx*O^BlopwxNOqEOCap4~btA{S&HY8(z{yo0 ziRtl=F3;m#745uh!t@~EG3;Q)5gxmO`)SiwADE&)X)s+b%%#j(xoFs$Z(7{IvC2FM zSx~j#K}RxfD_6tLkuyLz!gS_FX;t z=sarZn8-~Z;rM|o0xtDY8pdNZgz^&EFv*2@;O+QeQ}ifvU)BgQ-K3eokh&@JJ_9<* zLDifpAAlFhmZaUp>OYoC_d)TK`$&;S$^O`E?2h+Q-6GAA0)AA#Mcs$v^g$iYY|_;Wiy&2Gj~n@V(NOj~L2yh|TEmCWJcXUG9386;X(@AD%h| z{vaed8)Wf1a$mH7lXnZ z!rt)bg5IA50Qz<;GkdGp03*YRfaL`!(u(xBk4IpKH=u4Wye7GX3aNASoY_xT^=H+| zCn>f;Fy#&={2&VymmmQcD?R7N`Zi4yw&MIh6S_C>^qvR7I4oI@E13?`iYYgMnl}&e7v;;|vP5ve2k?5P z&_H%3GbhG>6_RqWV7k{|ja!R~n6W~;Au=QnUfE0UXldmbcW5|7BZADl+mL`B`ov$+ zhT#twOD{7a&>w#Qk5A9|catk?2X!VqQ3&cZzHZj3m?^cyj$*}XJ9g=C%ofFb%``_% z_bRysRc$T};c1T<(5UELHSxdpBzmwk5S7+`Z1SWXdd3iobVG5yP>%>kOIVd{=d1J| zNjE^>aTaZmmbwr(!6!dE^@z|rR^``%ryba|cW=Z0MG*;xTj(v4KfMK8&#Pis>Cz?- z|I)gLt7d>-i>j!!osg5S*D97*n=i+4^=blL!!3L}Blr4w79ZZToD3O+S>N+T=Bo!= zTGE0CY8r^6?v9thfJy4qx+Us=>OF>bj&xW)nA^@-xZ}@3S@^s8kQ}K{)Q@q=b}AwTS|f53IW0jKVcO=~qe^YH zvi&{#TfL!bUBtYuYoW!tZdmGRu-@(Oxi`s*)bd|lsFg#T;~pde$>Gf2;TfNP70*kc+( z**aeXJo?X>c#W+lZtconNa$8u>$Qm!3B<$CDiCf~4AhUE97ZI=!Stl{bVe=v3$j)@ zB=xGYj|O0+pTDzomD+D07)F+Q{Et`Tb&VyZ%f08pe8WG9KomrGif>(|SqDM69+A_G z<)+ZB<(j>E2;Y)GTbA*c44cVyZaz-^C@{8v4GLl^K!Om#_?ElULe|CVUv*Lv_lY-N zga-GQI{9qpSOqgS+-wfklN@EpeTKpuI~$S5qt-! zc6FjEHLwPP?P<}6cM^WY@wUFRHQ%q~A9Pti%`#ltZEpU&i10aS|M{$hj^#1!)h@P= zM!ek{*gPm(N57)n}$ys~IF>UHjL%(T{V>cmpdFk>Hy)#>U=3jr-KIX*}s8QygawR^c-%wu$Wp_D zjH);Qs<5*GNjaQF7bQQd6V^jA%K3scH5hlfPmw?G8-EUs8?6oMYMZ*dC4C4Uj>PY! z*Rw-@A<~Hl%wNrDRR}PipWC+bp;A!xlO{vjb$*ATXF_a#5jVVu~0JDGCeUwm=! z6a~F3FTAed_Vy{mS1e2JYJ}^nn75Iwh;*Dz!D*CqpZCel&$t@Oe$RO?KXF)x$oCN) z8Kp52BzwSS0|(iklLjq3DQo4l`nl|D#hvkh@$!`QTUaSkW-UBH8+pL$7iZ?KJ>Qma z+B%M%P?+UgzD>SVwY~M?u|hF+^U$IW#R?l%1;A8k5@Jr&p-^3UTGgQwmIN2^H%_Hh zD&aN{u?vSx*o7>L(UGbU&*X)c>|x*~&0$>=dBv2TwYjKHXMr>(Zp)}p-4`usva zS9SD*txnJIbvjhYn1p^E$GPg(5vu{Tr7b!T*o*smPxcYuc6!w~+qN`xvDBKBvJdLM*6Ug>`lr52$NmSBjn?SO zu6PZC@bhTXmtnr9h}rJBZ%l!9<9BIsGewN!XaUKnaw8VYu9bIgD6c{llK)h;ppb$T zw`~Mv`zPsBXSPB7jIJ^kJ;Ti&(c^oUTaj}$8F_R(N<94KCYkFqx%#E%%?&-r0m<7w zVOG)V7Wq^y<6b0meR^_m!y`8Ao5xRnnHjNqG-^U=1v97CGVAaY;)4&f8@uP%AeZ{f zC9a@Fz}fr6{uz*P$c(;_v8-3{kkn*9FG9OWvWT98>a-S-?BAM}ddR(43PlZ}RbnlP zc?~)T(}Dgjkfje)I9Iu_i>i2|8*cKZ=%Rt2{bdmp$dT38(VD7S z;yGMsh3P76xSE*1vb+Hd2}uzv-yo#zi+ueD&&i?Ln$t*qyeT6Af8(A)cH9d)#t~%u{6mBAnud}pgF0^C zGnu;K!DlMqG1@TXla1_RHrcW=%e{DjS5~DucKo3eqZk)7z$2xk@*6Xp6rJ7Lzu^E z^as(V6oDSiYz7XL%FJkT6X~6;8`d|NJKh?%k}(mUxUS7w-1$#3*yn^ZYVw4c4!QOx zF$&s5*l@iHI>87*z-_GY<<7lX`WzS!Eozf^Az9ILYL^p%6KAb$IKz~#NMb&NgJ?(w zkqmN5OL+huQn9(T(BdpV28E>b-*Oa)9t1k>31l4rjZ;b(Y*o8r-W??k&l%ZAVi{6b z;fjWvtT;e2knKK;LQ>UfbhcgxoL(FlGu!X=w^<2V5ksjvBq?$<%hiU%Vi}aAVkM5= z5mt05lyvKf5JZc6i#*K699q;28neaAfKEbk2^_nz$U$-fzH*vF2D_N*K(Q$#roJtL zI<(%USUt;h4ySf20jO#@mOgSpbnOJ4cox{OAX;c0C*JkQHPF*fGW{T=cM5q5Q0KqU zN#cu9I$`#t+$M}jg+N_%6RgA)hMQQz{yWD==Rm3u6f6+!$vBI^fch6lRQ_~(QatA{ zB2D`<&-Ss{>&J^A3bO=(viuu3i!!Wir0|v4O@KFwG|7U!+rB)+K!Xz}r(@4EkRotp zZCeOFkmjIo+q>>tAy)NuIyA!J;>N9Z?#TJUhk%YGOi1Ff#HHtTKQ$J*EL8(38A zW>71L7Q^+nb){`I;O(MQdPotYJU{wLvtS>%U`1;tJb zQs^A?J!L)FdqhQ>1KAuIP$X?TAHy6Om<-iA1JxWGl#Gt%Ut)T+vuYm*^=RqMEEJ_P zC!N#GaB0$gm83~7?kF-hxYIH{%mL|Hwa>8>h5)jR%in#r^;s%~x_M5CJZKoVmho23 zvkNv7T=_b(%Sn5dS7mg1M}W_KMep6NQC8o#)0yJs4!9*K`K_A~<4)Js`m`Q#FCWpSCE%zZ=>8)@Q+*v-USUOI%^`nBXbR+;t|AN zAEfG`3>-=_jmg=BA5p@v=Jo4_yFE2`1DkS9z!gx|nxjsD&_1Iej-{ygTVnw-S?rDxDi{E^AworrCc9JbNUd1Dsp z&eKhr=0YS0S8dhlF2}4~e)H8VonPG@O;<3Uvy6XN zNCLD>g1Kx0r!`NNh@G>0i=W;qHkX}8r+nxm&K&!vj_%L$TXd_#H|w?Pa(ui$Z^5}e z`1rYfZuII0lO>0mA{JL)JQ>oA?lM@Z3-7JF3|Ku`tUr4k<2+WV`h^)ZEtT27++0S# zWbhbK$2r0d*U65?$qp826w_JBQToxPKeNBQDHO9s6yF1u_|m@KCZMepkKbRvn;waS zT#CKpf-&U~TqMeH$JxTq@Y?ok64L$-QlwdQwcs8v_R(^qEw!B>23j}?(Et+XlMDk{ z%!pI~EXo^pTAeBH`Th#LWJ3ZIETL&fAbX-Wckk?q*{nLPZpr3@j9c(dC4Fv-s*qeN z#38Besp9Cjsn8Y{K?F<|L&6ixcDwf#ph!R+nIT?cBaxcMqRD~^N|WpdtCRdwdYur* znesjNzZ=(82_Yo_a^}ww3g6=bw+u=?W#yaHg;PgAS{8=${3^eh$=gLc0r6a~?#KPq z|6nGMSyc#~AenNWbd1nR|B4I3af_RPt6BPHCU;Y439-PV^}$_(QYXFJhJ>0BHYm$T zr}6s9g9rJ&F~+AIEP|@YmwrzRfCI_(k5U@W+l8@sipIdMkYn`(Y!@iJ8eGlzlJl{@ zq9v262y1*ZlUZqAA`zGUuZxI9sv~GP=_<`w$yMANo?mz8%lb9A!|V%se1Bmx+5*p* zZbBefeKV5_Tq-0K*J=H^S~v<)c6WibA|4qqRKcS)y;0dFaL}V5zzu9^6co)(*w3Yy z*8xmDuE$_lelm^3Ku(I#M8OP5pEdgLk0@XZ3N|scVOV4xGpHDZJSR9zHB6NH;W1&3 zc!r=B0}SC5*l`F_o?w1kt^*D*S~f)J=2q#asb-cVA%Ss{zRtfu&P&TTi%V2b3d%hh zg348A9n5DIn{*m)pMBPjpoM=3O*9Z6wh${c{bmbvUeFgLq+xH?enf*&g?zzvd%Ayb zd-v>WTdki24PbOHaimY22HOdsFJEkGW6Zg9`Sv?0rMRdZe!v@@^UHHD;+S}3h+98# zv1iL6eZ4`wTo7~Mv*8A?`|BXSlU(!^GIr_SgWhoJ9Wvsg^OeVvfYzfNMv1QL(4+;p zu{@ugv0vDmM|Ke6WQ&!Vmj-nO{emr5AXVy%lg9^OG+|*0DE`&A4qDuB%+>f>;nlGw zjb0m(Frkx8;RMTlP$NX8v6zm)cey!0=VQD|S&28~Gzb&c+44?N6ov(|WO~ygO{aM9 z`b)YA%a_5Nlk{t4qusl^mq33ukk+TxqwI?IjAIu8>K4ks84Q$E8>>P2%l9an9BpNT zSyU>@7i*f@fcq2Zx3L!W7>$^wEU|P2(v}ag_oahIN+?m`>NzXI3(D{_hBt#DvKCT> zEeh6?A?`&_km#t(!w94F#v~ufD4hXnpJIOm?T_BEMUd`_4@*c#DHX5=TS&+&dk%5t znKOlJm~9Ly??LmbrM9h|QBgnbaWFI;)`l`6&BCT8Oj$_llH0xP3F6(cICQ%{jb9)}Id~%A;WQsjsX8Ej@8B0@Ge4RHtcZE3t>u}f zS88`w(7nMfu7mXgFd;apZ3~PiX-sexcR95SCQe;I8UC`zj~}BBZf?yMhYZYHa{3dZ zgSw~ReYoq|SR!hkQ+zD6ZZv&Ae=@QvZBsl9)QKhe`6}`2*9h@aJbw!tFvgzwpzeP> zr3yf?1qxVWC5tGXV!203t(05-30iNukD4RW8Uq{`z(KQ%_qm7Q3Nze>7aXzH zTKaJn3~Wa9X7x58rDbB8dFst*cm_Tg7Z+=}akCU{)kLUPtcVWoqzR@6_op0^c!4ER zE*W@-0Md8b+E1Or2dv+ZX9sgpK?NAO7pnDuyH4*H>PAKPaSR>iR+zIKEOah!77uBj zD{S{JxtkHyDQ8Kd^2p{;DUdm%?kMf_vTROb+g+22L!^d>mO}Bz3ity(0(hl}jz55o zrBWxH_ju+@z2$O>ipQKTt} z7+-&p9pSOd`R0J$abw7?k*{Z^yf=%i_6XO|8mXI5M%SBPrt{r6OOhp_?|LyC{@ke* zaRepYq!{a|48*4*L((i96VcD9t@w!E&_6dy46bejIw?{bU7D;bETLzm{)=R7t|qFC z~A6gKqB^3K`d~%exyU7`ua6l@;JoQskg{#Os85j;zfL*J8UWg}k>281RBWX( zF8%<1R>ir9(bk^GmtWs1$#>~*n1%8K$Rv8V^rlUyROy>q9^;>@RbMuvcZ-PkOH914 zRiJ*P$MrzA)vv-}6{kGoa}g}d3fRGJ^HdSh#yuO~Tt43Is=ym#7>W-Ga_FphNE$`p z@H4jqBGalNNjIxQnCJFea z?ihjM34$sgibdN2n4yWfRpA%8OA!2{)Oq@W{daN7!>Hh9(o_<+QP-7U=`K#wB|`9I zqCy_;%t2vGTZy0s0xlTh?>yW@WD117XxqR5a!v)&UOX~EPUh1Hl{*q0wOq60Ja_9! zjbKAIYy)y*HSBjwmbDm>_R@DE6oJ$BUURB}l0O?DB(lk*jFXx6Gw*D(NIQi-b?7&< z-hCM>MOe|9bR(MCs69n6o#9Fs#EUyS=_@P2sjKSg-skXb+6@V{kx%u_+m=cTHMxBT z(#Ade=wsi2S?99I@Y*Fafs7y6K=`1!?Z65KQ$r_xMXJ=35(*=+33hkpwv6`$+j(rJ7 zXSZJpYv#2|`&|V_EPZMZBt{T>> z>hy_S14%jCT)2*WUpYsZO^K5usS;0e#Q6@1hZB4RH=Uu)+hw}cFBA1W$g_Ir2n|46 zdwEg@pB5+y9SOm!cqfk|TOfc=NyX!5mK?Nk!Os|mV64TH{3qG!H3=E|Q>+6oUm7({nL`_2*wf_gx637`DjcJSPRa$dZo9~%T?-!zWfYRI`}hQfru(u( zKCzeE$Wi%qg{so-8b)-Z0%%&N5mhHqaw(j$=<2p&kC=lJwQP5+mk&FOA}ZM|sgX7v zqyt774esu3iGqpq62)7&M2-OC{U7@I(7Li6od$n7Nb486ykYZ-pR%mYwA-Ky_#*#4 zCTBQ@zw`I{nI2eG$6DY_!mj>(TnfXP+Y=kTb!8d^RTYwx51PBbN7S$3mJJAN5u&8T z)-CGthKy^P4K$4oCUWA4>0q@TEd41SQ+fYfZ%NtpE}?So*CC<9_DV>_*O|zJX?czL z*%i>uaqAM|Y;>q83!l1D$>UN_?S9?3EnSv#h&Lae9K0s5SpDGX9+08tPW3Wx#Ag@9FA6Wo~@^%Z5UuI6B#sYO_kbw z0xBq3XjtWSX`tsFzj1X^zg+RP2U?4MGt3>vo#0Q4nNW(xd3NvETQ|ezHNPLgFSX0I zTrWb0I|lA^gi}+jU0*q{J8d7w@u5P}bIl&FgVNuYlyEL5n8~(thOkgn-$yw@QIoy7 z)-k9IXxAEBn37?_I_mlLO*c@6EUCFmS=|K(j{>;@tcY)MIM)!pG~j2Kdv?8Zdo4qI zf)Unxfhq)Ha9K3p{g`%G2X{tHzRDY^Yb_HWYqW~qE5tv_GFcW-z+F*%GJ?qZf0af` z+f?jv3*-q@I0f>z8xy$ZL!br=Gg#p{5Lmv#tRJ>gw>P9>J%MoZPM7nDb45?WC`G-d z=4O*Gh*VFYZca#5vJr~O1(sLSIEcV9Y4F3A2n*mhs1g6zSZe!4$7D)Jyg-XZfq zHm^;nj$T%CU`imsbTL==(S4(OLYmO!{fHxKR-w)TXa-hC1u241sq*LJLch^YOm$jR zfkeH|T*tSJ|1C2y4>CE&e6utcL{&t|_>*&EI3mW3A$8G^qt8M{W6;R%jUf9@9)W`q8~V;R^F=qtD#!<{oL-D>{WNZl=$w z8HYebzsJ`_(h#OFo%Ip_i}#m}bkNs@8w#glKeS*m7?ClgS7x90;vl1%vzm(JJZFSwR1w}6e6f8B(q-tCbqSkEjYvmi6!3;7a#o>MUpWQW&e=!8UVZj}7{ zVrFiX+RSkxdk7RDSUBaTyt;yH9`oij0=mcwxGYZH4ibX+9!O>vRHKxb*=nke@kW)F zU(S|!k!qp+xcRwU?Q8%1od6!`N*8linS(^Y@frCkLf}OXcy!@Zh>v~_Z~uIyM0`*i zU%s+tWVd|U5T8!}1j_z#+97zsSij(DK_ycLe&>kLNSF2K2+>MzlUEtA+GP5KgkQPn zjm%nBz|9F_>9?_*)IsbfBe)MFVzVLr7~^(WkuM+Kt)RV7zss;RbB+wD?pOdF=5{Ik z)Vl6c(QbQf;MD}FZ^jbDRucycxTPG`ZQN*D>yRw)wjXzl5i<>+z_GZeK1A_7GPPa@ zNf=Fv!{1wyc&ZUN)Cg5CB4OZ-%us=zy{94+JT||XP0p+R&5ciiS)QC(>ta2JXKFD| zIwnM5kq%@U*GOdgee#LlC!aXzv!aBd39ppgV>td%i&IPooG?fj>Sqcs9--+|bX4f0 zND=`EY`QJmPg$#Rx69TSz2~xlXt4I^`?0T-Oww{MpG_s~Se2!c{}}9yuR3L5FHQ{@`N&?!`@__9xFT0j`d<)S$h ziPg`*Eo@m0aTGCZY_H-XfOLetxAmT~&X^a{DeNyo#3TT#;gn4(FY}^}@f9Zqlh(u< zHt-;4OauGxLT92Fr+$M_wpWfIB1j75VME`g)?iT0vPo3# zcl;M#V}Fs!6t|`9%=ZyP-864p^ClaMDr^<=`0x%Y8ij>;Bl`f@P2R=4Qm(B9Fai@% zyS8w#uNyUxtR-faiiC~ys`;`fpSge;_du+J(os2>wpx72Y)&N8{@hTB{qxSiG85h# z{wQ>jngPd?#C?_SH(&vw(93l zh*@M8Q3^jYB7H$r{9fcSL#B8i4VDcQEgWc<%I)kVX4M(Ka$^gjPHsWenE!Lyg#CmoK?a zbok^LTo$1B@rO)1E&dGb93aQ2%o|7j{?Ar7mE_B9Pf!Zwn<9RmITAW)roMw8Eb(K4 ze+EgcQnQQ57uOsWEep}_aWGno{{|%yj{C4yOK{r5%>i6DIJf}|n9s(r(}0NoJP#QX zb5h*eEJs_JU@2rPe{t)$I|%abb9Li9Gh*p^epl=s!@}TLS9Gq>t4QxxXTNqv*ZK{> zJ|(aZFt=rW39of}V|T?_6wy0KmAGLfNl>2JaN5~L`iG4+rF=6xE^7<+(J;?t6imci z#3pNLw|rr{+zFfEica=tE-C4yg!OU^L4m05q~wU+@IZZVTUc6@_uqQWT+41pqNekN z(B6mdFP0s!(O{JVU%u=u>T7-n2ySfq!o=46(i2an-rZ!PmdbhvWp-KqTZ`&uc><{x zefBZV=U3YB`f*eL3|Z_SKl=tfDZSU)NCaA`ep4T<_yJh=Gjzx1jNk+bC$0-<(b~N- zZhzSlhscbznOO(aN#WmCXGTFR5Iaa2Z-~=bzEP5|gg;-9u!_z!iAGNw5c6uM34RL7 z-q;u$p5Z1j0VIWmYT&@CISeMBih&x}csTEu1+TJmZj(J=NG|ldBHSbLwHFxb2UVEY zJGLH0+WvSM4TNZ-m%7)cvO~4u8ZOvSLYvnxpK32ILVOJ8e${Dv{G{HTXir)Zu%Mcx zG_{t$qz;d@>Fgy*=?|Ux`4=qQ)(vhp+>k;me)eopf7Qlq+uamoCaDpq+g4#DCz6drp$ekX;}7*Kh)7|8D54-r7$b0Z-PmG<`S(REgYwPCh6>0`d!ohzE%D`tK7e! zNC;=8W|<@(_s;m>8CBGa&VLp!3b%f&`3^4U&a4{sVaw&Ri@q5UIoL5IE_#piGX5#s zJ-1;`>ew<>9z3duGpBbd8%FVv4o8iHxk{7fiZ4*Y){@}GpedM`JEJdzX1UV39!in1 zvX205M$^fa9uWoinK0?auHNXJNZfb55^IyrgH0k00Y+ywHnTiCo=pD4(a#)3i1*X=}}?=Hi8tnd&QXUg!qcaZ2aQ0OG4vmdt7 z0qa3>LUIJNJGlOYmFgujAczPIB#krUF9HH=!Hke*_UNny2hOhm8v56bLo6U8D5j~K zS7f4XCms9rB$yej23*l__rB}p-cE+vfc|e}uc7c-qe1Gfbd4d7stCN&KEq)t+48=v zj4a<2vV$_2mZV~`9723hJ@BkkNZR$|W63g6zOW5Q3Er$kXg)FuWpv`x1pOQk#J(_l zBNEP}W|m}^rU{CX3y_)a1(6W^#O^!ECDb1{4%_G)7R}ob3Qdi`d7#m~Js3n_q38_2 z1Os$R(|Ss#ntf(2P@+2cSccfqI5NQM#Gn7kcfckpc>;IF`Z=7>e!$*lkswC`_qe`e z=!HuU=5dE5gTGg%J%oM|wU6Ky(#%Q=cd&GNCSM{GnmEYg^*Zy$U6E_H*I_*$_bCgN zAp3{BTgWWbcT}MVh49Mk z6aB&o_r826B-#mlwuNH$?O9H(%QBiO;tXW`V3Ryiaken=`RFjjJpC+g;t`F?!-Ls~ zokDi7?l?B^du>~6NRJgwrY(riNp)PrkIOt00dKn)k2a%e1sJUn!FGpf4*Gf+2ZbC2 z517CUL29hHY3ab~W{=QhEKS?$pJsU^U`(w{86ez>*>&`yC&=@V0A-;%zUJs%OY2qg zm8s>{vFZF={=b4O!4rMd!9Say!^)K%2kgYiJEs+EoQ z7Gu02#WoJKU5v}#V!N0TM0F}5d97P7RNxKf{uqCW*=F+rqlC^{PV-sECtK~Si}Z;B z2&7C0-SeGbx?#a~Mzo{h(@6J29(H^bo*{h{lTwt}nKs2y1wGiNuQgSJvd~AQibGOF zUNTn-Ww&rp21hZR{yxlWp}`e{m!ffo9jM`W5k6G-FsvLQ9FrCwofTR&bt@fysmzRE zJDanVBLc?_0d@^!ld4Y^Ahy`VC@}) zI|~;*>)1xe_CL05JL%X)$F^-d>Daby+qRu_Fn!K@=G1+s?wy&MnQ!~s-u3LNUA3#$ zZ>_6A&GQS&GcDyRmXE?|EX89`UHsuX#Nf{7ppNGd;c*W2lXrCYBVdY9`c{Q8M^wrf z*&+dQy&yQTi;~9=>{Nyt@?jo$ca(|z2v1|TN+ZGy3sk6zLTiyg=}u|U z1Zp)^A&klL&P4CnMde$aNP0Aunl-%I*jK6Nh8_}+i(443D+0fH&@;VOT~??@m{cXV zb(jhxI^WfmIMmqg`6%iXTDhdo^db)x>j~Yd;T*~t4hzC5n`z3Lfcx_-2cVGajh2W_T|#sxfbW}aQP)ZV zotv*JU~Y-i{`m71Hd~%YBL@T!VvB8rQKas0IlS_yLd6qpQ~Onk$YDL#m8obA-wi)A z-K01UB>W2sM6bsx^2Q>pdzw`2KQSolW7Y<5y$fK-wj{!OnHe7w%>`d@(Qg5-I}`@0`|&VxR{ z$8fUY->GHILPHotY^6!$L{YuZCY1yf;Bz%P)nO+yE@X4`x|QMIdtFi~2W$za*?RtQ zh_)P?ND7qJs4$|Ft1*~cG~qFvqZo9AOGNb@vhnptHLk}K>%c4+jK_IDep;!~-E?WM zS{=fXV`$hL)XWyh&^_$;2kyb60!L|m|9PRC8jnmd9XbxyFk0-t1S1UzdVU=vt)%HF zEjMKRAdN%XUjRej#I?iT$qQ`DQdl3LmzAi(b<6KDM25@s7`HlB6Xr+kUS!a+J4;&2 z@nC7aBAPiX-ZZ^tiTJeJOu#ou+3N4@5*lb1ukjc-?Iy9P&qY}mwvnK-p7VgX(dCZi zOGu46^Hxne*53r-0%viJN<9fEI*~LIz|k@DyVxrkeuw7hN)f#%FSVq#IfQOeWM`+C zmsQQqf2&Qmrbc(hU9f9Tt`X|PJ&*|GgyactIMdRQ%NfgQiREXLN^l3yTOj3}=C-4k z*613S*bE4wo(Cm{oCG_W2B%Pb4)CSQ9kt43_P% z=^_F?c>0%vKj!H2`$ksnBXhQaxpHTqbS>98O+eCbKxvl+3WavdKMIG;cB4LV2{R5h zL}%G^uKhmotCh^HF9n(7R6rsm2XMb-;ynfsko6gB?@%-9^{}3N4}Q>qmTp2PjNTyl zIs6@kbvjIWPa=HjPy^GXf1ch(btPRjR!nqMD?}qa(6M!i zbD@>vR|Xb}{P5EF;eDkIT@pqu&LQXZnp}rzyr}V#4W(wb|Fd!^_JbDZ>(FkTj`QUA zr2&EJJ*U(l5+>+(Jqv8rZlkiPE2eyYDDgzh!@5kYBcB}0c6+XQ>P?(e5F_P_V`i@t zDii9^y*qJYXO=w^6|~t|(PHS*8dSxd@q5{2yrUrbtm=|OToZJ?EEob_ANKg&R~E@C z(-$V`iEd7Q_1Z~kk8sLa0sY2o{aPW+ynH`4j2vKgxOgp{6bfJ;SRtNhNN~m1B}8sA ziW1(LMP)(mVbEx+$|fqCF#0&cvXg{>I$Zz+-a&Z~f#^p(xqqjBJ~5ssU#|iOV8}JO zd2q(a0!ZKW?+(uKOz^gt^7<_d>XyN^itx0unu0emVUr)+aDV8Y(8lX4Xv|%ZbNnRY zSJ63bZ{Eu)@+5+juR*YZOUNJ*>ba*2nWUF%84Xk9Oyz<5V^n?|$;NWe{!J#FXJH<7 z0=(L1c9XyUfYnV({me;R3hJw&Q=iA!JrmvdN3I$B&Vdh^lL}LDyV;xFVtrpIb`FY+ zKY>CMWdf(_^?8RVUcrvhx{|Jvkf!!B z5CbVvh6|OO5-dN`Uqa+=@oJu~fP|y$qs8yQrLM0OCAg>SAQ&Ao7;s)s5$$`IHYp81 zJ{W!y0vADUJTUirGP}eaR=YI5w;03r4$4XwSBCzp(>DHmwGSGkj0_rGtd+`+@^5=xGVtM8u4tIhljaz;gMKbXn#ByI zJsf_C{NMPBwXX{do^+^~Ad-NOj<3-GtV#j$EXZ_~!jR%de!6#W@Ou;)=e7<2y zWU5WPU|J=AG6`IWtTKc8og%ffj*?BJ%>$l@!Yz6-M}#8+pkZ^(2!(Lby1*?hM(8~1 zz2(Hik-&)WL&y5Lr(sQmfml0GwE@J-b!M)H1A@}jzO||w+$D<^uVY8yoG2~2oCFhs z-H$UxK1~X@*~%NDu(ohWHBRGGZ=mc;)3He$PQ-S)1Dr8RDS3ufq&M#UV!>Vhgw&)m ze|g2JW&p~C%_nhcDVc-?+Zpy1H%hc&$7)_+y{IX&1VksLCi#s9qXX!v(w1 zz-B#(%sYT?k)cp$L?jtfC-ebnH)Kt`10dUF?%p<~vH zu(QmPvTK_`ql-BJ3z=d=Ye}^IB@%HxnJjF3J9CW8;nbaj1Q7|xgm}pqR^o<>(AMoE zK`$m&w|ghodaHk%syUyIEBP#+9?hat=;hv#8DHnajzxjxQd<>eqts3>Y~>$y>3pbI_*s4 zQRKsL>(Cp^c2~2(hp=H08rG^*WjT?Rb3W_Mr(QGfJ0e-%oqf6@tqv}hHWYFJy_DKp zkr$k>Vj@|R;>b$UsF`|*Gn5QJ5e8MsHsAaDYfQPNduE%+_^0H^u-l5fo-*>g#dk(z znm!5dM5z;mmuIy)8~R?dr-`X9XSm5PqcIGrr!<^44}AOl^7h+)SAVUZv!6u~&)a5j z01pV5wM~lPPA{E8xi^yNf_dkpq#E2ut-4fZVMu$=#m_O5s(s=t@{>W+8M&qUsjFV{ zUh4t&eyLq_(61Bd(UqJN9*mUeC!|(8+DM^!P6J?j$H4Dyrnzpp>7r0ofF+~p{;G#L zvIMIxOPmKs6tn>a9acGI)Zg#7Ygq5u0@E?A_?lF91GoH9e{#+!w<5k-j>n$)+aJ^v zMbz0k{w|H2RmpE-eEWH~z%b+YDj|D(YkT%JfW`o&#kpj}45C9)Q z48f|U6CN68YN$n-yAXSx{CuuGBC*WMMmQQ8~ zMKaiL0lvSg*S2z%b@Y_nxQrHhR*UNux^h5?o_+j;V}4-xufO+@y3tn-J!Ft?ec~8*J|z6dWh^u?CU7S)rY*Ai)|-{q8nJ zf|c9aQZfA+swIx=|IA>E_ck7TJy`}}bu5CJo$$;DxJ1SvzET;@Aa!OA(pj7Tfky#J zUwo+WU2|xDHukRIFsZMffUDvfV6j^rR~SS)EIo_$N`gAP=?P;Jqbe&?kb(iuo6d0N zsJWKi{$2E>50CD2__IYoBHAlM0&QssA%`lu*IVM85Q8=07aKZ`oT$fUzP6V>VE$mD znlmfHCM^PeE4APeq&=}c|`RzF6|->u}v z*vuuOd^js-^UQNIJ-h$?p1P1=lFe>*UU&$a7?a`pQ0R8P$l@WZ$inEMzp=^6zyrmh zT&W}*IX7YvI(We)Mpl_Yk=D7sO8UX2oGk>{SlxmT$?(9%G5aN}0`CEo8ZX4Hd`lAa zj~#OYLdy0Iq$|YKY$E>YMQbi!ZMn3I&1?T(jc!9%N?~nG4$fF&OSnLpgyq(61(``H zow)t*qcQq14V_u%e7-5_Rsn(sC{^b{_&t*C;Qc?DF>2=MKQ>Xj#oD-fiv-{u%X)|X zxxe$f#*;}}{7-8LaocH@cZox_ldHYbB3$V9q^yQgLM z_?xJw2#zLCznSo^1S_#9b`jCLsz0lZTS|ka9>^g)y_UObO=5*^w3{hNlXMc-@E2Ln!J0@J#fS=_)(Bv9bpqtP#&Y6 zF>ddJta8L9K#E!K&!gVwP@fWkC|jvPDV;8f{ajFIuuge4^aWzBXGT9o;Uf{D9AM@z zNC<~h9wqS3v2iIs+=VUSf2~*hsOa|zIuF2GpPKA2Qq`m-%1ep}$gR*pZb2(DUeOg0 zBPt4)ari7`G^Ogj7*@(HT@${^Jx_jFQnHvIAGWcjobhLlMOu+N5bL;ZqN{^g^Nbj_@D;~>q1CC6<#q^92l=?%1W zM@D)pRIC<^6{(N%HwW%`bQYO>O!^xFVZ57^_r&-w( zD8R@R3Fj2i+Wi4FlC~~>l_?)RhOHqbI1E!UY|zSvyCtQy6LZUWCpuS4`h`-gZKU@y z-?+8~1EhE(j&zozm@uZr_$W+DjnAfNz#?0q}`PgaDpTVC2I!*X7o{CZSb=+kWY!aByaMKt5I^78>I@C`dZ$D)c zAydv9EXKnDJ#wPSwq9F@tmv5$34k7@QQ$SHfb!aeOqw+*hw@q_6{__J!*V1_JrJ|N z;ZSX`_>@T$xiK0uy0kxz=#!{EyEcGQ8fIjJG`x1sFy!!@dORlWa(*h@P|_CvSmrAC z%WMFo@0YO3SI$>~rh69CZt#gTco1StLjGe(1U4Qk9Slx8Qs@{FAId8si5DS+eE|@T zY=s=!L8m3~bJ@Y-bpmbZ&?4KNPKpH`;ipQ=ZE*qFv2wdWb5zObS&E6LiSb0hE_U`D zZ6}fN`rb@aDy<+O^*lusa&#hvjPXyD?H(<76h`+B);l(scd0E=IrWd@t{aIU;roe- za032$fU{P18+xKKzB|s+OcS6&~XV*aZ7WhN(>&zBbPDrNKbo{$Tb z;x5m#h}5gy=0?Snp0teubhd^H|9qDjI>Th>M0vg8Z4Y;9B z0tUt^*gU>Y1{MbR!KQa_a5*0-+D3LOeZRjm9y!JO$ZFD-3$r1{)11WUF6JS|(`dVn z5DF_2dX)P*+I=Jd)tJVN;=-N5ly^p}4DJh6AIM@-a^pYY02u#~+4-+GVgKWJkK=y< zA*lWnLI73#>ryi{{nvKS6RiF}yYYXpxcwVI_}5hL-v9#R|2Me%e-Z)uzu)ftpGAQF zd*J79?dRW4wf{Me@Sl6s{&^Pv^_%(^j=;gm#{3^WYMtsfzcxmZzpC}>2*&yg+J1R) zf&m*+gci2~bw^fz>Jz|dB-k`HmySB&e*c-CxOR|~YF=o>AiQ_bGj%b`Nq0Yf-Ky5n zo5HAadpLg8*_g&)uPEZ+&tcWN8@U^ke2~pjS1m50Y#Im9R?cfEM$V^bRG-7JdHb|l z=QdN@f2=HdwXKIAV0mXx)vRQ?ci6J+Uu`75lo&cNW#Z=gOb_kr9tv&Jvn3v^(O@V| zR+GD@oCK%Q4x))`PU4GZFM(T6x+|O%uoR_j$xbKsl(1XaKhJZ}iNrk@E7pZA6)c!zaA_#|D904Ct8W`VMc zVQV?xqvd@u{egB(U{^~TI#I+jE6aN+i%>e24$tWm5p#jKMj-V)<{NDs*#*j*(*u0* z%Tc~Zxx~(AWEZ-F7T09%65lx%cUh^Nmwi!s1dmy}oj*cS508KXjZ%WUGU@dms0s!Tl)0yWy$m?K%^D%VXovJvgt zeSZN2blLk|WTi+zC1N=QzG(vWG6Osy00LSc0hw})#hrChYI_3#^VVe)%-mnfg&fGq z9x99InLpddiyjcWK#!`54wS6~;s?l=kt<_ThVXwutg|0=wCnc-99@@kbYrZfRl-w z?wzYwH6f1_4$akjY9`*9>wM9+j{dCp>iVPp8TnitB7LQUC!~9h@NmeloEnCIPeAGl z{z?@YSUO`xko|=D`^j}JLN)v0#)U4|pXv&@#~Vy*@d`chPXo=)%(hEQxo6&{UZG~SwGJW1e_qheUvo5{ zEhC?%j4Flrn4agq2QY>+D%I-fP0N&wqgk88#ygi!`StxUNajGgk`KT~V$lyoz-2+dt2wWEI4XNJ3T%oki=COMq6LKNbLy6?N`upFF zP^y#t&MIt^T^<6CvCC1g3l+)nQN!gOAIT{YKx=#k0@yMRQcl}dAu?`a(S?0OB5$~R zz>)mX7IJN_hMNbge-gdN!ika>k)E-0i;ocHs@fxu$iPU}uCjQ-roR(l?%;gvqMf?O z#>8WzPKDqKK#z@}4D*lxXd>8afEOIZTiZ>&f1T zmKU|oyc$WlUR-ZXMKCDfPaQl{(+WC15G_sQFmME$rso`GEph6PMYrRzkA&h<107n( z3_@eor}C56LkLo**Q^>SuGE1v(rTpu;4a&* z-o7QRYlEhlwBp%2osQ=p*x~i3msKOcs&($Py~U7{fsKYy;^4oq_v>;yrWn*@ujo_l z1t1OLH3=^IrWDi=x0?`LkDxB~kYS=YG4b&Gk+bBe1O>?#=(H;FOpAl5!4Pp^GU4fO zN1hlCnHN=^>X84mfkq*SApez4#s*w7FrD**V|iwz;xnaKMcjJA;Y?Gdwv>TG3LL_d zcq4K>AEKc4SwzJkaE8t)x;c$HAaWHay>Q6Z8TP^Yj9~cXl9@r6i&IiNJYY5Z z67Bqsay~!YeFi^FRxNK^Cadb@q=fXUFR~dZfUu^Zzd}mNroDtpy1C8Rhb@|i3?k3y zmQl+x!f@(Y3Q5tLY|NV9V+L&yPcwAGfcj-jsyuN^r&C`%-YN!6BRvUDA2-WcW}(sG zHb}iS+q{GjXmliU2ho?B@$0skrm8Pr%Q>PnCqyH~QM6ybv`~Eknl4L9klo>2lr&dl zljwW*V%z0-G>~XpSvPQNPF^tt(4AZdX?W0$+&C z_HazA{1xACy>`flOI5RK(M}sl_2VviszH+-P3tILo`}oe@Eaq@q$5$`@Ir~Gy zOs%I>42tGxNRFgje@PFSIgYYo5y=STx%yDIM`R{$l!vMj_m-T^gF0z|=Bn=A=cyDf zwr7|OB7ViM(>oh+-Q_)8_1I}!fpNeEi8HbUZJ-EIVwRyA%VX6Te8~8@k{f%#m2-)`FhhJWx}q2bqCWvtuU zEK7+7_h8m5buKia-3COtDn&g6V)x72Q|mGDC?f+-u)O{1MJPbV7}KdL+`dNwX|q#+ z1JRnaAiesVtvp%tV6>lhc?T~(U(&sCaoK<6le!a*son zvds;1y4u73z(Dq+;|9-2rY5K>qVhoI28Hh_TqmbE?Pjg}T|?#E46_CP-++mpWCTx2KGSj(|C?PzO&Y!7(fv7O1EXElwJk z1E3`;TB+?bELpIq4~1n#60Uc&wl&SG#%%;HDL&ZLPz7re2T8*a2n(MlQpsgjfvUh; zb#b$c9sn|acNeOSU@fQ=wA*qJ@vx((Vn(u)6bf|dkq#e|Y7BI#g5C=VQlt<}Y(yML zx`RtmS2<^$K+yrhC3RJ16Tr>SL5{C^MrL*z(kcHE_H4Quj3=FQ?JhW^@Hnh48y|1i zNFm#JH7Lr~*r?5bv}&{zHqZ>PHODsX)ayZ}xYtY#G2H_%r>15mK5&>0N8x0_m57`T z5LBf4{TW5~Gm^Ots+QTaP&rb;-K>vZIaRw_%m1!(sP0vtfk(Bqk1nKKIHcU+i9xu@ z1#+(T{=mZYj=LfR=I*5l)m_&CticA6lm@yBYqF`R-fAQps(JOy|t z^@u@Vuj9dXp-PS$BZSG(A18jK44`=-3>_jN{WIYprUrxBR|y}?iE!Ya4;*rz;wN!< z%Y+82;XW{#eUydYA3AnlE-fpZa=vO^%80`jthaC6UixffE;Wglcv~qzwqC*p0?`@&Ss@2 zhEE&^G$8lBU<2wsApZ+Z%%YW3K{go>kdZ-B&SLMHT&i8z*AKvc@i;>$dy>QS^8ot_46~5)Jlb#%Lb&zn9U;t zez3PcZj#&Ei1n79vM|t0+$t4ghu%k0h9OFgkXhoN0J$9i*apfy_ZMHc+v)73YSg^ zt;F?5^hOyZTc*Q^oQTr03}N7s(h^m)C@O|1k_^i2c1f?Evxg2HOBbE3d^OGv`O2U7 zd~543TT*e+oDxk35h-2@$F~@mUm-`rCIekty9DR!Wn>|FH@b1)MIE_kW#dzlEL3=_ zF64UbG&o!hMJn+`BuXTEhhnV@2sCG^AjGyec(suyReqjh8nb&C!<27e3S{RS%A6nXwM=FR#TVxiL; z%{M#(A)vd3+j>P7eR5xpV<1dLd2?I(zJx;$W3{&;8m3!oyM~)F{X#kUabF{C4ANn; zG*@-IHM)#kcDH&ef7Ct69Jj{sqPGu9?vPy6mY|(|tKT1x2F`L=4acL|L9teCymxDnBS{T`Xw@mAk%3x%tfT{fC}47f!jVH}iqK#e@fMZPYa>)v@m8+lq2+VvF8k0KGB@Qzic{CvH@_5x z?0+ATTR*wc@(K?_1R*H6q{O=cdoi=@(!S<)-7+e8?D*Z03fcYMSwII+1YsE?21*-r`Q{QwMO4O>iO|xvH=iq%A%$VuDg^m3fE^- zg*xLg1DlG_rQ=8dK?ZO4qLn-eh};M?hAtBbU$atv6YY>cqE}>KPAUQ@81c6`C30)+ zDv3|ntT*U@2t1FjK--eswVnvjdbM8{aB$)nL4u00ScDAY)0_d>(AljY0gYrZ-G~pF z2_b5KUL&F9>kaV>f`$j{YuqjZK@AfOtBON<4I^%U7N2Nmrl{g^nH3>2>*x7o#rTH( zkgPoSlgTgmdNc5TUdTOayssfx@|`q*DH0BrV3?ThB&#;xo`W{7Vc^L*FNf0EqSCgQ zB@-qusNvX)D_t>%>#yXS_pDBrH|CXwJ*u~5kr~CWnRO2goDfS7lqXNrYi{CQrC>dF zrT=j$p!H0lNsR`A3`EERAf&@W`esdnIMlvH{>jV7zUx3J9)$`R4&OD}{H03_mGT%| z5=~I5t(E@;E*$O}bRK4kPg$q;@%HtvnycZ}abP65Tf$Dy<7Z z>MbLEnCFdpn4X^*U_&suSREJDi+K<{pabDu^yiL!ApvwsXXlUo$OcVt$t=Wns$Y;c z=jW3a>&aD0e_)?7?aXYdj$M*p0*RdS3~73h5(MBvg0BX}4WPtyIrbK#W}GZ{;$xUB z59cPse<}iVxN>Zc#Tp||IjKRD#mi+Zst*&BhNvsxeRCD>QPg7>>O(+|26c zIiTns-QBhSaf@NCLnpZNNRiBBgEOI5^T&=o#+O8gTOG`ci|#Zpfy;%ug==g_rbx9a z1jA5#D%p+zEQWK^@_4g(3-osI~ zB1pmMI2a3V68Tv!cXwvz@(+lp`%oS=;IaO9ZI7gGdfgP|;>619H9PEF!18oek|tin70b`&pMJ&W^Oe3iq3r^AgT%bp$DoAFii?KHz5vAMSt@$nrYPnBC+GZ zfb!p#^;zOUze0Oz?A2}60W{CPCB-cP*Hev`l*z&czV<3Y#9Lq>Tgyae50e%*j0M?N z!i^!gZT>+B3K9KnA)To1uE*@oyImqNU{(~V%|9yH7<{0=H(%TxESjgm53>l|0*Awf zgdo*?XJ-9NYJ@afSxxSb1y~CU5&|Yj6uG1~0AraM&MM4fcqm(J@2tP?quNKrZU{x)n-7y05^*suO_|(pi2bpYG~LcKWFxV zUset-hQ?EyaHM*08%3quYBm70&{N98OkcvqGrpYWZ!!%jW~}Jit>yJzBf7~{J#CbK zJR?0;whQh&pL*#;%|>*C7HBfuASQ~+IUAKR#8^l%+^(4S=y}>`Tr;$Pw3MVo8!1nK z@>tm*{34YtAH0)5g@=5M+{B~JM^68-qO?&k&I|^sN@p2#646oX^uofU+QA8&(L&Dh zE&}@l_YXuwyjYF`F%)U_@N7@THN>S*vJ#M5igH8b*eB7<>VL)M2k) zJBlMel4BzD*G+%LwT+FLY#Eb$CV6>Tgr%f7Pjl{A*=~x>x>(o!_1Q{Bz2cefCAp)7 zIm$88rr{)xt$UufqgM6X&o*pc+joDQu1exBHZKdU~b#f>%dnG`IbUeFYh@pS8@lEgv0RdKgn=z>5`v#%}RqwC6cR5IYN|gFDz(oWcrs5{-%2)L8A%7B@7 zDl3;c@Gzs-42Nf}wRYx_&KimmV}*NsdS7$4;Fx!sOF8^V3Gd!|`Q%_fe9kyc=`hL< zYjV)OjZAfKOx<*YQC<_i5!TEpQ*yml6VRpJgx}u3vH6)eqN!^OQBFZVo-n@RZJ=23 z++bYD-LH^CGt>GAF!YXe6E!MP8D>Hlj&d}yzMo|L5>ofGA}N-_A#ECb#8ckW z=(t5hU)b~zfb9+ZhrYDA8p1NM$r=_wi}2X~O4g#QF`$F<^~f^|t3FGqBY8+K3z&;= z+}1o7XJ|_&^sJ(*0^wPVQ{Yd|XFDa6pBLw(l;bn{`sCI=Y&P`-o?Fx^RMa)+sn_If z44`9wZRk4QV8sb+hP1)UxP!_(=Go{^{@5&-qclmYF-amXBcP(E@fHcG-VdyI2|Y#8 zZ?6j@(wV_C=-8}Fmx^Bo?TLVt3&+XmUSQSL1knYuW-n~?Wvw_aUdv+7S?o{cDh;Wj za392-8M1o+8@rZsoroSO+oC7$)kRL$Fz|lcZt#`*J??m-Qq-Q4v{Xi&p)982iCYhZ z$Nk!m?;wU^xHDOy8%wfE7D7|x{Fx;tcEh{|dUbxu*NoJa<6Jb=Mt!EZ);?U6`b?97 zGiGBGdg5_5ZH3ykJnK9$DYc0-a-+yy9=7wNK+y9sBxpCFRS)L2U~!-~zIaRCGUkIN zypC@ut>lA*!|>@k8g4a(+>@%dRUEz3ezz)I_Jw0A^cVOpuG@t76iP$AjWp@;X zmg9c3jSBkS46c}+rhcE(PIYISvs{x!Ts7OQRILo_yc9HjQuL+=o0#M#Qbw4RHK0a= z)LdII%{Z#^JmZu8AXIp=Sg1$>nnxde{Ey#@#{HlE!Yp71Zi}wqleLhc=~}mbhB3FP z(CK>CjEAdxGpxj6Z{Ql1?3$90 z0M{hgiFLJmGH-Og@?XnZ9J%?^0xnqML-q)i9tg8n%4F*h+|(#87_@^74;@)2{)qWh z!0;1^pYQ^dHW&;3@Gog?Y0Z8Q4uaKK!g*oYFAO<<*(wbdxLBhN)^LjOIT7{FkU zj_3n|5iV&d$9IbQ+IUWK80>xUE5}!;IIMyQL80!LkE9$8#vb0jZPo}E{r*WeqX|5Y z%2ka|92;%l3dn$1%vDZ}nn64#*{g0DtR@*z;3;}aPVql<{OziyJYCBb4)G z+mb|eANM%Rvw~JJydlkTTK_5JSvjY}_I!<7xar4~Le=zwO35ONGYe?Fu^%U@aw8Bi zGHv3Dpj=E1K99srjI`HeqMajgyFT87i-^G! z^-t;k0%+yHBt!($qF%nWAfM5wf3oT3srpW|=xK}_GJBRm=ivVzkMS~X$;NRL43H_QO1SAuMkZIPd z9bjfJnJd&XW0Nz?NBTH&4PNHsyb6Y(hu)4I2#n@H!nsx}#`cAx=*dGW&Ygu|>zC5S za0Ab3vc|cUkpVnUuBhubyrE~PD_FRE)mteRDAGwg!FluGdrbCJa`ak!IR0Ti`FHiaay;c9QMQpL4g|fiWSw?(-eZdd~8{pA9G&W4q7=_4N1@h z;z_c4&Yjc0ZSqIq~~j%Q|mEEk^Wd6J<(bdk|UISWfo0=&S~9|*ea0z-AxXcWs+LHhhk zNYxo=W^f1*QC+4~@Zh`X;;bg-pE>ymGwq~S*B!V^*7ckoecYE38Ku(FueQ`09o9^l zvFG|nmFX2yU>;{MPd-EMJqK?HG@ni_hHvTF>1zxJ;mP-cF-bnvFuVX9Q6u5I*#)8EU zfF}mrumdqEHDb+@0=dLSOFUDjX-D%dm$#In>E|Bz&RVcm&3Q$pWKbg5I&IJ!5zv3g><4_-(+B5?Z+rTDWkDTdAMY$Lk-lW8F!4@cXU7?Y=ITPgb zBSW#o_;}0j?1yCcf;-*z;o4WYT`*)U5YB~G-+<=_^kO6Nc!i6;wa|WKFnG9C8>I~< z5V*jAdB{!^F^I!O#v`GW(vTwA?NUuj%dNk`An`C&N-MgfvrT(=c!{AN&_|n~Y(oUa z__O~0s>QFlHLqr79B6JH(REN7>a9}`R$J4TI6NBrp!~On zu(WKnVHZ+T#6$5%GoI$gTHe>1-rG$cap^;P_643GqDAGsFL6fk@}v~Gw{Pl{?^-|0 zbIhi(3)h3ck;6&8{0s6n-mg1fcvs3Eqk;6tR7Lc4X79CvHw8Ipc#s7>NpXU!iP{P) z%Xk%SbK_9>MF&2C96D^fOqZN_x0B{$8%Ms&CZZDIFIbHC*~=|XG4n?_07C4?t#7>Y z>2wQ!EQ>6DpeL88`D%V0IkjxMfmXYKUg!QgN8>Jx4<%-bsh9c=yItyFcmC1TNJ_`J z%u|lc``uiEFVO-5|vKp4Q-@Vc{s3bFw){!NCk6l?mI&a}vYXw?@um-T<6oay2(niHM>B z^Zl23dXG~^{m;;&f3z9@Ym(-FV4j%S{};@Y@;{lUhJT7zj{g~u#1XVGLc)VZ(g#}i zf#&$np5Px+l7BtHzu~8U=wSY%xSa7{I+p(z3CaI^`00OJT>k&FulzeF`8Of?zhPow z{8y+`!NAOfkMAD`_m4wj{O3vh*Dvh9n^;&m82%&rG^wuPh%-IWd3rK* zH>TIi>Egr3nAor$IyP(Y8u{Y#d?5esKi-D@{j(r?_OG}+Sh>z9Tsmm6EYv9VMSWyM zXL$N6o>pxwCwuC&CUR)Xql2URf!6csd}_my&7C}Jo;dVOOZ*Ox5rfT&W~YNl9CJjJC)PU5%U=x% zWLfpLT|{sUP!^JZdZN6DST-sHmg0YBGDRsAQ>&;lCFZfKH7B{p3$x%88xhjr_oF!a z6XhcfeUK|LnzcIKam`2Qd7Ylh1%<0B;}uEBX{1UnwUeC4VhoXS-H7fe`a$jbbF!dejt2QjGZC6d;$7`x*jz zXSLPH2U1dd;v{E5(hO1Iu1%G`#Pwub{apU@ym_##7c(LTi8zr4JNY(!4yJ)PI&Y-fesrA zFdi$EK(VE-mJQ#o&OLqPyx7QypEwfB8qdpmhYCy*eXajl1AiP{;0>f%VFy8~s;MK5` zvRdA;LhE!^NG5pQk?PU#gAvMqhfkaXOZ}~qGhut0B6S- z=e;))l^8kTjI;!TgLGh(wPG`)vB-(Td?i}vDs;`&ha?bfAamKG8=SolxwX~n?r|bA zEqp9Jt0p9|PZs^hQu)HL z9so=;Iyk`!S5xw8We!ZBk|<^p4wP_tyFu$(1#Ua4Gsd-oVZr=ei-X1aCs=h(wuErg`v7dUf+BlY)y_EX1pP z`$m|9D(`P-Y7jE`6fxS`OgcT7a!p>rITV}UWqf`vJ+`)*|o+5?g*?(rr4r)poM&3+2FvD9V^4_;0M zmBUDvV@gGE9IzluO7Y07vB(tVBB8jZ9pb?=XWi#?cl#mh`J7|wL#LEHUMRm0Jmq}l zc|gJ-_eN;FCL|;4;P}Zjb9&7{c#enIbxmGM;#hL${1V=*?@MNDje%Fiby=KpVNEvA ztO`nALM>=!@b|sT3s#U=&T5=xr>*RW!EQkMBU$=^i*p4UK)&&^=%L3)R!rtzaWNix zqF}PlllO%%W~2)GQ@!Gtz#DD-vGA8zP6VHd|2ZL{2tTCHyFi3dH=^`}?tWKz$Jd(N zjvxMUg|53uaK5pqg2tb*~r(p{=G5-u)+Q}>fR~1ws&9G zjcwbuZQD*pY}>Z&WW=^@+atD<5!*TW&ziMsu3c;Gb1wGHx$4zg^{UpY*Vn53dcE)W zJTptHu3U&zc%9y!>_ZiKMooBQ8r*mX@BJsYpSvrkz6P78zUUkb@LYD9wecgG?()TO zcri%^$<*Diq_i10)w^pK}CbQN5ey;uN{=a z(4=Z3>`6xl33UjIP$tjWbS$WBCkdEvg8|CVAPJFtL@S@mNH_ET32do|JX8#_;-}0iN7(S!fcfK9 z!`IfoBS?IOheK1su*n93?8apj3G~J~53plvjor#R522fIw2Z#=VUzETg7U<-Pyj3U z-PZaP9o(Rszyl)C#aH~&9&?c0p@{T3Y;z26f&)(_fT7hsF|bY;A|fYr#&7s*ookb( zi97JJA%^fFezQu5n$+#3uV6g7mT&jELLk6nFO^dYII4%jKK3EcmsI6~v(TE?S zkE=aDG~>qB7>Pj1ysLn7@L|psYy%{*KvpW`lUD+}sxhAnlLf}gR%U<}dsV(Th#0C4 z;Z)aNVj6GMx&~I4sBv3ZU~EnEppYCYkg%LrX4ZyV!fjxcdjL>c!OoZ3M2b8VmwS$j!C-;Jm=!+7#rrQi5HB&s(Tz-?n(XhNt zHwW`CVP{28mn!F0K{R~$D(oJidhxR*xtR#$V>d+D(scS$i;f;5-8xNT#ND`yI01I^ z+AWFR;Tc^yK!+BKWeQ4rSS19V`V!PZB7cbHle~&}RN{5{b9t;h*mNuXkYEf;VPHuc z(E?6@2}g~)Ay zAG;8}H`b|p7Ov?10)1cb8Dc&CHK$;}bOF8h@3e|UD#_Km4STKbnS~fFQ;Iie>gW`{ z(#oZ~WV>aQ7?`i&U$>R-z+&icA(A*X)nwk0)WH4z4z>dcoV{d~{h{*Dni$%7?ynkD ziM*o8L!3MLm9KDTC=8Sc1_|3Ery?*uSsU+pA-?s?G&wD(2A5@lav+om0SP|dx{)MbodrgEPK_Fx&LUqCH{ zSY4u%D}itv0RYu|qTRzcVa2QgUAILaAOs=BhSpO?&3#~D509T9_}7G02FmBmL^Do^ zNZo17PM<$*)54xDp~=J%Dgfk5?K)XIrR*^-tgZ|p0U|& zL2mp+xX{ioIy#?l9sPVxf{G*}mKLy|uZkfI zC$SuQII>@F1-1@$8Y89gTwvYT$Y#0Dq;el$uLe{a6iL~+Hh(p^3bQxTXFu6CZtNsJ zZtUN-b@#iPVV#C6o&SEhi>HBQMlVHUa!yMN!(yQ-yz3(l!hScG{xXNCxc9YQIRtOpV<2ub zrC|YZ0NRzsvQBcqMSB!@zA{-2rcmG^dY^qEJQn!6p1`Jq`t2Ta2lvxOZVJr=z9gSb zfOrQWbD3B{?2T+51hRMc;THf9-LP=8xViCF9b+{LE-~e)4;-8?U9N#ztDqF~ydHce zLA8=59w;9ZUEfPylf;_uqKe*3=s1gI`P19E1>=(cMkcUKtxe|Wy7IcXlPOtw)6`;t zaP&^NHbBm@;tD5>O|u5&XZ}oLRtk7pis(DvAEEjiyXJZu?K)1tltI^Zlw2g)U~m-g zDp6RWy8dO)m7Vj)rbqO&aBAz?&=y!3%8q8;vovApW~JR^G8TS8m4uK^7PI04&k=w7 z4d@a6F4I4XwjpcptZ3QkK9?XXEb**tF1AF6pPMu-%#!jGIUdiO?O{BOxCd|RIBf*i zpjL^%2h$=OtP{>ZeA>pQrdXm|p=-Q54}FOb)zdqa4Cui>oFF4w-P&_Zd0BJ!n}KMx zl3{`K4?i-SKFrAU7m^bD4czu@$|Nso@MAY|$Z*`=hjKlgfjG0#KWS;UevuYCGlq4L zxo>5^$jDGaOKqqvm;Ao9ETdOT7UGQmxD{`+d&sKLaOZviBn%n(9V*-E$fROlW8c58 zf0lAo{VTR#!ufK7$6!MCJ|BE7o?muH{a4)OI1UG3>x|Wa>S3-)bajI-OUdWbR+Xa; zrG>BJAo?oMk1R|jNL&BR?v&(SPU#KKFK$c&o-3ms_E_cj z<&Wv(0g;Yjjp_;bEc{iP2raf?_8^Zk=(F82{}D<)-|#-g9soAHh3EHS^lg1e@m9Zd zp1}%X;N`~O6w7JCi)`zh2Qthz+N^bcK-8xwhu*z-0c?Z@6F0~({i5H*G)#uE=ht$ zLASe5+BqcUFRM#Q9$;Ij6C->_sMlPbBe5(G`-1O>=&1LBsd@b;d1v~-e{rFDsymf^5`Eta8k z?crMw>XYHqA6XwSH{EGFX_;APp`CJR$Om&nWVwtJ%)2tt3vzCh#Ef?!cWjT`0+<7= zh8fhF2XK>slj=`dwSKWNMLKS#EuA?dequ<@#&q?H+z2xy1|3i>2_&b(2A}yj`YeFA zs2V%LjRG#@Gt)V`dmhVK`yzRhdY86ho|4qB4x@KKi>>XYwJoNk1?>EH@4s#lPm#jA zz==kMRnxD_9WZz|`gVaWO@8o%{`kGTyF4cJyy|?!<2>?v@%U<|v>w7-k$Tb$2WO^c z(XU&?3-n$i6Q>D-YaVS)JdUYD_R>n+`JQgHAK|jczbHvbMGf%?nB7hhb*veW6KvH_ zI}Oi}y9xI0VNsd#LAygZV0GP1`eVZd*Pvr$*+82|73?_QwEe(a|2XFcBqsN}jQpi* zaEbJzz^17Jbk;j?&6eV^vM$===?y@+FSpVV5fzOROJ3yGF@uI^1w#Us6Z8iC>~f2e zm1Tu3j+272EwFuE-W2(qEFusI)ltXAsiRhm5UYRI;Mt`-^ynfs21d?Q3Q8eoZ0|zE zM)RY~H<>FS{_3VJPX$rJ^MQ!0SWBrRFVw}e;daN?c>`N{D?Nt_EHj#(QAxvzhb-&( z(j?jo(*72t&z-MM@O~sJ)EF~L6i}6r+t$;+_2MDTW%#z&F9M6~9<6&RjB87X|3FKG zhZdKDJ;5C&ud?JBvya`|g4p3nSUOaCZ3o#S`i<1Mro?rtePf>^M@h{IBgn^?5x<1s z54s5>BShOv8CC~9W}=ztcwn_fn@}V<6GQLXim#hZv^8XS+5X#fOJuAnV-&W#RFfH+ zldwgwc*s+=-GzoQCr%QOl@EBmLtLZ@g3Pn(+ip*@&*H-JFHe>gOKjte2#A>;(hSQT zanQ&@6*~O$M}q0TU8i~!l$<_>DwLzh)~J2zG*%B!V*n-#_bX!Ynv9iV`ZNS$sAsTg zjN-^BGohAenG2F>H}7Zc6UzEq2_aAX4GQV14EiyEb{M7X=$0rzs*RTaH0?2}?<3H+ zg02O4v9p5Hn6vr?x!8x2YnGL-Y_K|x!iWM6!Hq+Cl0ZoBE#>Pm58C>S2+an(y>Q21j^3#!Ulj=>rgQmvHhdh)D<~U zuwF!+1Zd%f4`m?gEKKLeIIbWxuV)Jko0ahNh$|&qYKBR4I;dUGx=1~yj)LhQ8;4Gp zTrdkgBzHE*IR=3LUQ3Y3Y7J_sw*j9!SZ1m9NQ8p#+Q;hqrKAzoW{x;siXRqWgAU4r! zkZfw~3_&*8_>4{CEo3B=p?aZKGhr*?5X%}tv6O0qle?J~{7&^`8zB@c#zR|l^&_hf zWNs4_VjBjl$v^wvo0NC)rNXi6!NhQr?_g)h)BS)-l094k6Tl^aN1wNSz*K^SF~7fl zUL9qjq~bcz`oM-tz{qlmP#=DM4iaPvxKa|=FVHb3rM1N%9sTf0W^j~Nx7|&A_?{4+HEsI4yjgAuNv>~uUzv?j zI2AR2FmD!MnB4;cG7Qm?iswl?7CK+5{iU7D-Q2YLQ(u&LIkQ7k-OGxWv(YAD?=_SS zk>!=D)Kjo~mG(uwR2*k$w00kdu$wenv5t|~q$SvM`6ymE`E^RLdey0xx{Z@`w#+1@ zmvAlbrmN?dLl_}dU1~8GFxL$q;Dii!vCdrIH?IBw4b~unrQ%FiL?NPY{Khc1!Jh?I z-<3>mXS$qDpr#*ciB(KvJc~D@9lD=GwY6e%GvV4BmK{sMY>iYv*M2x9tjyVXsDl63 zy56txZS2fH;9B^#4`+tWf7Nyz;H3|>eP0TotInh2MNk{t2d#_mfU2;fItUfHl>_3< z&q?PJTb9&Ihyr1~xo}NOzn$m=00e6f(B&&F&Je2I4w*HN1`{uKZ2Mz1z)k@*>39Wp zzqs=TV-{kWY|E?>#ZMA=xdRbB zZn=O3MAUmbdQxi$1r3McR?t|JMJ0WR(Ns6zfTheC0xm4sw+0^}WoKv9GiN~jB&q~P z2M2g7wBBvQs&bHg0DBT3*(EVRa;{Wng?LD8Q?cgFhuKlh^jxP>Ax*u~Ax%)^g?mOx zwnIidMALpA^jUbVHUuy`@6^ss{$ufF^5Cb00`R=7qU+s93sue40A^cU+hCWq*Hhd@ z6cI&J?jd*`F`Su%Va$g=rOdn$3?3j)Mf0U6rc|8+2SIxo*r#|TGGv^})QJ!9m1*B0 zQZeA1IcWEC1wt!jai})Lvn_4H7|(;6ss-H*uBMgacQODkh7ir!zZNw=&CH?`7uM(W zwos1Q>-BZeFv{87ir|z>_PnD*EQTy^p<`FpHcj=u3!FMSJs~{V+K%!Xab9?q1H}`B z7>7w1xY|whwy`K5Wm)1*5)^FCz$>a)V{&byPVRfkrJfDC-`G0pz_H|K5Px7FTGu`| z>{3x&wrI+CL*%YC;pfMz(CU`Y(E*)Zadoq!vZwRg(;v9-C(j_oBekUIfsV+IqQTn7 zNI7uO6PWV@E9aSZL_C3z9ZR+-=6e!bqOw3CN+kpd$H51vg(#5*$t7I0CQj2H0Q@Z> z^B%&c>|viG5o*YS7Z1?;mNQ&17?GkEMxCFe>m9wZU3crO!?Nkg_T+N`9V>{k5&`IW zSZg1kFpOR3ogxGwN;TfJ=?8|d4utw&-*R25!gj7;VNcR~%5>+^&BHv8D%gAkM>Q}V z2ZLB`{B0wjboL5}_4oUU!IDau(NoN6xG_wPT5~h<@!Ih8+Ew=5d_)Cp8_YB(kF1eb zqme5~$roDL6>eut=Zef7H9$q*>>(pro3`X54WSDQc2+iT4Pgfgc09ZvBb^}7foCp_ z*9`Pwif9swp+eV-K$4PyO_Nl{HfzAxp`p{ajC?eZj1(`WSxrLQ%tFP)RIjrD7`HK3 z2@WvUM5|86=Euy*w24Q&?h%bQseV%!H=RK+K4q&)^Rr1uuE%~@s}bT7mfRB-%2*NF zHv1z^anuY(Mb|fSPGKbI{uLJE-5ay2JQHOotTtm|8lmeq7}?-dK4~{6xR)iNVA+$k zV71myA~T*6mGK&3U7i*^Wcnjq)dCaEI$rl09ieDNNoXWFW=@0tBumf@0sQ9};bSKw z0$$mkgSM||*YEyh@DU$vKbT*nj9Nc|3_C|KcaoI=!a1wALujeKNZcJ>>wC=w44f|| zZhJpu$_>_-ub}SeTWPgm@7v;h5qo%U)JC=`_FO+cKQ2AauaGLOUL*4piiqDb2HaN( z;jKIs>LCWFWsNP36Kk`I zTHp0L!WJ-LIRALIlIhsM9J0jqUCYR1n*q3>8h@c`8RH&%_2QYO($&XNidl3e7?0A8 z*UdSMyKtl1MS|Qj0(vgL<=Bah?nkxAF4t3*>9aE3@GWr0(s`Z#cz69pKl21?0oT_d zojHG#5VoVwpZMHmw*RngZZ>LO*)*BKp9-5KJ8T=feaQ6N9WQ887NNs1o1o2w-{2v{ z3=uy190c$7He>RIa~kf>OpQ;jWW8I`SF+LdIY<_)_nog_;d{tqQYmNj(%f5mvka4} zBk;x6Ijf(Oh$>g=to1o!^DAId4-QffvLP~e#_jn~cAoBTknj4!_(3R;t@kL9tn82* zZ5#Q4H9(FTMH39^57<}Tm;XfG#Rqf)3D@kGU!+RJt~&DZ;Hpbq-Lu0ev7S;f4N5FW zh<(SYQYQSTvfo)Q{<~yEjwQ6GIuRdJA9g2E>dc3#9zGS(<%l7Mu#nhd7j>b4sit0+JxURi_ zB52M=vWuD~a8zPRo#v?H=-l|7!)ax3eRpohA+6%MKV}X#WV3}25$$csC*c~!2~0$m zMHv7HP}8*reZORlfu+v1rOer^Tmx&;6&$XT&q#TcO0K)T@_KP=9j&vYZF=!~H*jCl zY{{G#;*LlxjS3bLDwC>K7kMsJ524vDhrWM&B=5_T0{>ki{tx=&e~x(nw@m*3RU)qb zuS~x6Uzt1_*uOy}%OK7F-If1C!}7nf_ z{}DsN$nxJ>Q5gS29P>Y0Q8<{{*#0w%zfhO9+vGs%c~`%Ypj~K^kU{|k0lZ5WZAho- zlDxj?&k0(%iuFop;&J89nX28XD4E1@$qQfvCaIG!|Uh! z=Gbeerv6vj)wjdCcI~>fR=3%&!O1ElwQ501V_K>>q+C-~jYz#YL>B*=@3L2~cI|A# z#i_l=pj7&e%Zks=ZQ6I^YBdVIcDu`zyf&4*w3h$a+GWyl#8$mCY4M23ERmL-ynW1R zFqOKcFTr%uOD$@>Z)6kpJX=+CI8{%6=G9Jfd_}Y0j%nDgG1IOwvb9r&YNlw6C5`6Y z=QS<~=jezcQjfx<;mA*3N)d&^*~8;UjdCcI(velRDUu=Ydf0w$_d_DFKh5r)MN4F7 zUlY3h^GBo>NeoTdASmNlAYF*qk+RfL=tquHO)g|Y-#GB;Ip;Mqt7ZSkr-b!oDsUDC z{gnPLcQ2ROf@3hHF)btU<6=<6jkGBtpDR4ynQ>_Kl|u?$ocbvb5zIT*!Ku`b z69BNUlBG^QYO_HIeL%}zs(5w!&G^1GNpBn2Sq_=O=g-yi1_%fl= z_jb#BSbT>LmDvJD7*+vUF`7|QHY`)^{IW1MUSb+B1ek}gn#55T47oJ1b$^AHMjCK! zW61F(t3Un84s(hy11re@&%&nau$MCl$ER=rM|!5{oPQ0Oeorw`iNVfAlPdTczbEv(66srnr(c-oImyie35 z{KqB^HH@Y-c5Z$bvsGGwQZF527=4Y$(@=ah1TYMIoffq#@k1QCJBe7D{^EZXEg}eZF#Jqz zh%e5%gjDyc+`=BN2p~`aYrHR2&ITv&Q#nf*DN7JLCNc84%xMvq)vXvJZ2)F3(NBe6 z@Rh~|sFJ;(Gky^-Y{*FY#{7hp@YLFtnn*}OPRq6gh!}6vd*D-afe6cd!K|UXI16?G z`&S>jJ528sf}9n14s5tWd)e1orzvSx6AI5KXiAAv{!pKI-vlP!jnO{U?!2k5B#Q$hmS-<9Wfv+%A)q+ z-7%jcEr`_>C@#wiV*?hzq2se*TJa1-QxWS>&K5)XKkaR(iWxweMYml9gb-Bz93!Lc z(gTD%1_Z?GeIa>Hh@;sGQR7=>>C*t0dhk{h@M1U`$jEc$Whgawjc6-=XyMGCWcEBS zX*bDl?sbSsP{fYgKcwC}?kf}l2FX)jFY?P-Qb0{foVvybX(gM)2(pI^slDO1U_UE4 z`s~RPUnzG2)OCRE4@6fC&gj3$Lc8i1#razXqxqIz7=B;efFIOKL@XOq>fGz`dsF%W zYidRM`Gq(L!dI&6^(bUj(H5+p0W&F2g!#;csfX|>a3ZD5fq(bdcN3AK`Te|1N1oY859u-gd0P!t#vaw^6^0pGIo6Q&hb>h%giJ!|vY!p&VcMeR|G)|3ES}(8D-ZYP&y-Y!$mH?$E;mK%3oRgUHjOXh zUn*KT>Mi4bKc+?2;?=bpF7Qf8moKQFOQka|-2vW;#=QU`@_Y1jpaIo#t&u)MQQAP# zgm1Gi!ZE7fYD6Ws+0E^*e;6H#>2e{fj^#!sh-wfK8tpMQZYki^CZzo0OR)(lpuvll zMx{3)rma&xe^-<`Ne2}HT6^r@L&M(z;rn7V`GJ^pPvg8dMLzV(72AQ0UMTkrQlr+_ zGNh9l=;Q+D_LGMZvp}a1pDD5`w1+f&yhllxWqM>qoJU%2;lQU$c%3^x#q6a+7x~To z@UZO#dEpqlmea5$n zd%3=Mm!BxD<&3m`6=4`Z;HlZ)-n$7C*p-!!Ot-#2Uhqo=ly!t05^|Zws|pr11=9$+ z{+GEuQWq)ak}U($Dj5=ke+S67#LuoKF!~i+|14jyi-U)xFt^}H#u|Z-wcSML6*e>E!cE1;LJf>5aB)yl&;WF2Ef?1bNHvR3;(lHi`Dq>kxP#(K#nYI zg!D8f;#(E5W zfDlRsjxUfoUOx*WO;e&1y$@7B!2m)pVqzw(tVqw+W{`NuyWt1@tBN?Y@1 zbhF?|$d$ik%T>KDtli3o&XaLb94Gc=V+MHll3SQh>sfl58vJ_r1P)FtV{cysWrIg& zQ9tzCFT=gflO`>!Jwv}Aoy9DU`WCfQ3fx37iErerM$6-{d4l@ESHhWIGS>5i^Yvd2 zPTz6ShE_`9UE)4$kL3=3#E^%(Y9#&QO+YdnPnkh0HYL(*AJA0i# zU4Q8|0K8q)p7PEEV^2T*$uQn*4!mRt&LcdDB*8owijG4{oN!33hbZO!@cFuHQ#w0f zvJW-39^*(eI-)LJZj5x>QpA9cscSiTjDpzllOcg}k}-kuS5u)#iMBLS#*n5S-IitK z7zx~a$6^43ps`YzO&O=8u@W117{}(KH~d@Q@lZo6RkL+m&J(&c4Zeyjw+XYto)v0~ z-fv&n83Sgqh|L*8W+zthHJMtn0n=lsoM}^_EFA~kP9>jrV==%iOiHO1-aATIuyPf?m>j%N;(t{tD%{T zIur(tPT|7cC&X}f0n9A22G{!xR1My812!$o60E@Fmv!0nn|WQ`nuLm`MD&yIRZ{ewSF z<4uevtLcOOg!q z(xn*p!HiAm0xlpTnMO)aUL0`-RWUE&!_xC_Fe+zzb7Z_7P*q~Ul(xuEJxxox$3*Dg{&J3=>ICMvTF`Y)Ab+?- z$$3W8Q=LINU+f?fyxof7%bqV9k8hzlCWBJG!E*yY57%at7lfR@_-WS5LsrC65pnMx zg++RPh}U;6i#sA+%%FaEW9SH17>A2#Yq1++W+zaub(3>rVQhG>r_J@Z1u3F@o(@k6 zkDuYfe5Uo&y!T_H<|DxU+{pEn^Hb@CR5RA<>Z5%aPFq1{s>Hd{3C)uPZk;jrA9Tk~ zk407CuR}GGr`jI~#w6RCGCmmM|*m@es_M<5S`4$r0fJ|m?AR+GB`zdePJ17W;29^GUN-xvgnWC zGHxQV?zk9d2@CzjJXWp9lt=N!OC!e3$<|DC9Qx4ujrh?ZKowxa)fM?l$@E$-;8)1Ssj5YC=V`W3t$ zY)7yYMjYHL{92Ofl3M!0k*6!_j^obKy5_of7*#3&zxcsC@wutz$y;~~Ht-9EGZ-DF z21%PZ==m#eGvf~eA6=F2T{Zb;QNdU&ea_KzWLyu z`2lCPF18t=mL^;&`e;O;TIyiNFx&H#6GtH?JV+L}cMT6!YNCbJ(vZgG`Z@-RjZNvE z4r5yjvA+@l_2_jFqCe_gOHRzj0QI&=(Yn;+pMEQVEthVfDmeoM6J*Y>$$Rd}>>}*UyH&xaMUtcPt$3L# zwG8)>Or_-USpDk#ofSYTC%_b~ufHDf8$c1qRw{!FA&0uRH>;r~e+h{sHXo72vX1HE zV|uHIyWC?Oh&t+C_W?souB0zt1yv2Vyp(A1rf(eM$o|XmAgv9LAUM1*CDbZcjFP9O}?e~Oc<`CP28Pk-nrF1Hay;##5cp0A0 z1Zv`sfCLkMM+*^thK56N`sNJvoOknTdpJVCn^(s-Ng6Y_K#j&5Wf~?XsIOK&=z<0t&zW7BcWd0f+-lkO$vl&2XgKl(<=<9 zaBrQ=6i@&QHb6|MBS}Hh_X!XcPOAk4B6x%u3AkhcTFB%F{56G!5h$Vwk#PYO^0eGg zWb#I>VIt1c!y;rJbwRxE(lAHga?OzAuOk;R0ALyE@gH99Ij4{IQwPwD~Yw0Q2dX5IwGi1L`)T#TC^5yWYB@!0oI7~r+8 z9_MJz9k3h(=;IODux4U9sSIaao}Xi27}?Svt0d**blq7`MBzaMJcyk{0e1Vmi_K}X z*(OMI)TiGU)XGk6P*Un|111WLw8 zvIjHp>dGWkqVp5}ncjH4ch|2G)0Q$M-&|F`h}1=vtvOQctC7Ya>45spdP;@Bu5=jr zg~VDDkrZz=QH-GzLfEuRHOHeo76{k4)}hu1WGtEaY)I{RUib|m_yAZxj0{TW)k#+- zI2Wj*i21df7uSW+dsG)XZZqG+7=`nxl`SiFZr*qNt3MSW4}Ad%@u5fgVT!cFzvMgI zaH$a=?fdzoi$9Hue@NArO?Ys2M3x0Cw|{7nxt&Q4l%e55hAw6cHJXNVQQGD><-}Br zItLifPeh&PU53UeHjv|=b#-7kb?L*x%Jv-pG=^^5sh4+b@Dx#8@3GR8khwTQ8wtsd zrhLC|g`D*)3ANXd%>yn_Rh!E77_xsfIqEuA!z7L@Xo zK~C8K@IE@xVq1EuZ-SFY$3jE*)C88z1vaZL*Ds2#&cb{Zq$FUOX>(~xcrW5cfE~M) zZAAWrrZJZy{5#~syPM=R?zg?;ud%s+Zu z?`a2bZp$s2ma>S=%WeYD8IEBj=|*mqn|fEQdsGv=5Fb_pX{+7z9Hm0-1wrz@?i29QpV)B39SrgJ08w4dez*x#0UDGjz3 zux&S+YrlK&pDYg0Bto#Ew_!)neeRBSU{B`U$K3|`B&FW@{DO;YM$~KzGKCh=h)Hey3JUZhUngOe01 z+pg4nPdaP@O~kVb7{BPIFuvRctU0VGr<68;$xrwchmxO6Xn^I+jSM1pv$gOvHQb2M z$4&!R)9^4x*<59Wk&7|oL689Px$0SwEH0Nt)UP);Gg7Q;?JylR=ngFE?^#|ImKvyC z1q&asd^9=z1yC5?hP4ZSPGwTLvW+Tuf)bAAl1NW3k}}9AG~oB(l*#C%Zjr)VIZffd zKr0PAD0Hs~4<@22@5L%NM)DFBP__5KP)&(QaZF)}1gi?3fMpyGnjIW*FZZVZ#rrxL zbN@cu2+86yTn~+m@xw94hcHXXCZB!`&~E)wbI+xuXY; zY=-y~CiTe5vM7&%J0SU1uP6d`co2}?Om49Eksxrq!(F?K{l}RCkauXDsKm82;i>K> z9pX?SeI{&&x>}jwJpyXyT0j44MHfBFX<{8Le+JR9gn)_}lEeyZp?VG+|?kUVm7G zt~NP^jMqDc5pS0b!*#lBhz@l%0g2!fNJGbDmJfJg_Q@|iM$hh^hj7ZMA8&H@M}N92 z17K0IZcA|DV{)vea-#tXuQIQ`O`EjL>B3YIL}bqpn62Y+9i zi`nY`My}&5KC}}9iNz`Zc|I6MtH?X}8C zBjQCGQz3=U4LBts$o9qy*Pu*5Vl2I->P2iWEi!{*Ubtd2^0;u3s4N}6G&p=tRO;uH z{PKA*vdS*LdbP#f8MVee^Y_=@8|5KCVZSz(3TnS`F=Q?nRL}fnfG|IIRPT4%o8_4e zHu%8v7ZfgO9;!&BC+exe{FR|R?VPLp&l*b^@h9$VNwWR^oM9K1Ocv#u#X|$M4ilbv zzlD``6zpM2Hd$TFUKZVy>P<9w_Y~;?uA8Cd&@BXBou}nk171Z(Ls$@OLYUFE2qun> zC*-%F_=xkD?4g_R1jN9yg4YuHnt#0Y|CHLie#HD7F- zSoN8_TV^)7LNW=j2kQ(Jj%-m$fcGQqFzww4dL+!BrKV~2j={HnSw=V~H$GZ&-?@o* z&ZCvyD27;ey5>=k*Id|dl~<8FDm#!aISj2;Fyt#mxSn3rS@HL+G5y^c-Yh*XTn;}| z4fo3KF5Pi=@>H?ay4B#)(dgmoHdEn?r;axr721n4&B{qbDyo{z-AfiJ=ayLEo*HX3 zky9q-E#57WV8+r_7o9l2LUFu#r>fd`M13N8t=AKbrrYyj=LJKuw0n2tw^<0550+Nj zcd(vX5NW>dpT4cT$(dp%IboC7uSF6-HSOF(DADPsknEM)0bSvb6ulq}!i27|P zB3J;EZ4nk^%!4D2WbA#+%zl{Tsd@zT=Z-+SKV5}j?fYD}Z&S+#HwnkxVffw)a zP~l=sB=56g$z;KOf;7AkAtvyKxLZvkaQxSUmI*+Tw+1N_9OJhny^5v2l%y&pExOY( znqP*H1+rb5K{epz*S%%t9t8dRNVut6l^F`M-jjwz$vGzO%lsLq4BORm&M(Nr_XbOa zm^Z4aY)!^YjiDPUMA8(#pqrXm^AVv7^wbXCLTPSXSLve?Uw(^pcsQ*3&htRyxSU9f6Kv{u)wWBHg4|L&$eMe&s@o-MMD@y6UHv z-AfKSY)*=u5@?~-pEkRD^(O=F|c2 zrxy*wHtz^Mf-8rvzjLc|S$y^6-mW9DAMdXU^1Ees*swY_Pp=gjgyp_6vHKCP7wu%f za)Pj!JN2n{DQ;10F>!?BQ>Qy}hX9fXkccO7G6gzT*lWes)cU-_9VF{0@Rtwm@X_CE z&ouKZxP2Gp(EfZ>L5ui zdqQJNMF=y_=zgfiU|C4&fQMUGt``~-J_2e}O8qxHZtIgpD)b7LgUIBQDv3QL?Zpqid zHvz7#+vN?h409yc@HuLipP-$p0+JL8HgVB5i53>klQ@*y@a_h}Cc+Fg#rqXXRX+v7 zAKj}8Eswo+M%Gw}w;Q4ni3Hq!pRa8@#*&3XOi#Hg35Tu(q@9=(AzsyAjIkd@m(1`! zAc&V%);0L%6HgC5(>Q3nea(ac9BQ)dypeGy7vM^b!>P`?mHpFB89JYY`0qL*oc}l5 zDRm0}v{PCcz?6P8IcQ}6Zm0D8x9t?B|M2ttuXf5mck`d^6sCV%IzK1=Z?seXVPh!K zE15Xixi}h`IPvh%3)@-SIV#&57@7QYpv*wP&cgN&zvAbD%8dUU{EJh;$44*rIgtDdYiGDG+(o?6k}B z**;HZdDNR-@5u9AJ>T!w=+vzlYuT*QC&}klkh4uP8K6ivBd#-48X2e>YFa82y5#iN z+rj_*`<5Li)0=oL*lZY2-ljWgoqY7}ty;bq43p>2X`C-TGk5%wZ7>`qG3qqr&6Fwq zK}B?9N)nn3lz?I%Ow^qbPm1RB37(;~8(U;IoPC;)eB){AiZSkpWnLF#(-$VI|CO+q zNe+imr+G7ZOG^;(M3TIvu1+$4n@vDZig? zCE5p&5h9a*uqpizD4bpye^3!$n+Lz4lFHb)?UNjBg`VBjDNib7#?oEQ4Vq2MC4*(> z*!Mg)CXK8_pfIqg^G&wxUwLo=+jfZpsb@5-4HHcCt5q6pI6GcHPwzqdOdWxWaw|T2 zI$!;kF z8KLeb-a?7`>>)QDS|9?%I7y?;X1@ud)sjN7xS_L7?OVHGDVmKfDVIMJ2u0fME3H6J zl~ccgilso|37)d6IVlV|T+Xz&>zF+-LAD0ID=G!rJ@%*pjs^&v)ELcB8PVccRc>Z> zF7WaV){FrQ27 zp+uqAirvG^TbJf=6iWbaynHO0eD|w8x6uk#ADUfJL$o_`DD^ z4Idkfer+l6fZiTg@J?^THL^>wYzBe!X3?W*Dq1 zt7*UVv=*HymvX6oWgrG1h1eDvtOqIP7e{q?3$ z6Lpd&Us65P3rEqpZmgy8?09?Bn&tupR35NU7HU-(5^y6jL+RP?qE%cY2QJkuT1pU$ zRaR-)R;QKmoCullHcP$W-c}A)QH@2{>X6e4^wwOJrG1_^;5!d{?Hbd}`Wr)Bib#~Y zm-O--C zjbBA9rPtaW0o3cfVc%ti}_B$513;ffwL+QS`Z>_2fVHgN~XdDGjv zo(sugvz)LNauVPQN##na8IybFz1p`6{}?oA4A_u_h*z>I%*}Yb*rWF>%5>gkpJGon z);a$H-yG7f19{Va;TRPUNZQ$Qn)Lu1S;q4Hkg}4JXZ8)MuuC9?i4`ln^;e)L<&0dO19UkT16$l z_KitCdZUUQp)||;%K>;tu^oNg?`UK{) zw2fo(D?bF{;9?zcQuzMKHcqPv4#fWlT!c1gA}3g{|599F7BhQKF)Xq8VFy4ZmOuPf zUK|9j8I=F07x%Vny!ig3WF4>(_slTATns%Z9jp*7SU|y!GLK*@-xA7j=1yEFvS23@ z@f;uFu`SH`9O0%R5%~Y1?w!8_`}TC**tTukwr$(CZ9A#hww+XL+pajN*g3U(@9y4x z&iCxT$X(; zxbKxezF2_CZMfYf?4F90b<)_WY3V9Sh^tw+xyb_cx=NJI+$3v(^Qyh_g zl1PiCJlyU88jd>0J9HMd$FpBeVZfXxu;Ak$=oos2yTwk3rDv4}@eG7akYH&icat^g z?+X82$_Y~Z4!9UEQzTlsxOfenw&#`qy683*yyAf+j=_@UmEW^Cju4FTRNP(Ee`&)2R3$6fU_mjK zo7H~)lLpxmuQRKuGgY%*0iUu7Eel)-O{C#|ueCNxSEsUb!XrX0VU%fc2Zv9JWN;IE(RN<}BL>9qrR0toEX{vu~wW$rzaa1~i<#JiXBsbKzWHS1V4A&M+~ zQ{)X2PR?982)y(&3h+5_!(GjOX*@>(-V4qc=k-m1a9r#}aId>zIze#@eCa9}$#m%o z1&=*UZ$F@ziUeZLdG>v!N<8)79e(2F*QwMR*l$M&Pm7A#@}TU8C<~Rx8wfGDNDSgE z-B$5HweSAQ!V1~MQpR%?0b^A0Eb(zxQ1mh>V_KVf6uTljt*3!2L2I*VS`_S0nl#P*_vCrf$h>sFt%+hRl6{zcuK5*0_lN1`aIl`p!Xi%sIVDcbmB&B@5% zz;X1j`1O4UTrv@fG9I@GTN^3-$T!=2w!`tzO8_~0i4P`v*mka+J|F8gcV|}59#KpI zCy=gGW21m45Y6UFfnBRDwnoKht}jdOvCc)j{p$G zd}MH3)&-O6?gzig!wiauOW!IzTJiV_CC~%TP`>Sz*sJiW{n5=O9#fNcr!eX`#&NuX z;X*&x)$POQO(vvKDuhvVXg({hCJwsVw!3pKiUkmzrHvPbNjVwds&PPak?6`)>6-CC z(XxJ|U$f>VD%2Y~p0;!st!oijx7?z@eVQzFWA?|#>~FO56|uf=N-&T89tGfx7$4+< zfsYH;P8lR^Hx1QE4S>YWFwsx}t1gCzL3E^>^}ReP19@nk{U34Zy9En*USx4HU8b`* z8$Z^X@WN@8y-4NZ0)vq(=JMT0-nJ3=u>**8ptnB4zdYGawS?ZZCX9;CTXCV*_gvgC@;-HS z_}@WWb8~*$M>R>uHM%*TK|;g?8DUrf#Mrt!2hOAKUK+@E8)At;PrV?qp5lhH^gi|J6AYf>vpu=1W! z%0wL?=q6~{{mDWesHJeHv%lrlGPk=915*GEcRfug90wMb5Y>;mLSe}KdRRDk|Us#6B}t_ohHQigN_kVb;7bP*$uEIv9L*)9aYFQ_K9+OC!ITKXuYs(cLv1Q$!REvziP66)I%(XpPZI%_NcU z_9$f((GPD+$;zL=cd^qsgq%UlKSY2H6jxa|G^ZQfR_*{n5Z{yGJLYJ;QErT+YuPwi z;?A+jyp|a+9nG0)JoB@Ukr*fIT#QP2Jo5Y*01y3hj!h&&@Ge(*um|`}UF5rQ0-|J5 zCIgxjaW~z)OBRx}bFt-GSul%$j5-CVZI|N=KEV{LgJ`CsIU6{h*jaH(9`8bga9sgTqC2451aNqneWyDYAYi9`Xxi-`6?&c$RTzmYgo zyYvfD{GE-vZ;&84^yh0$6315J6z+u42ReRRUL{;I>_R0{28YAjyOFkxeI9iNu9z1+ z$rr+)MUjR79WCd~n!VvVY+}UxsO-p z2jF>vQn#%&(AWbspotdAuV#RQmTmy4n@45p8Z%G@uco{w3s&j?rYuHZfjuX$<<}HVR%`DZ#j|D4cihYlfCVt_Hnm2)j22dg#iF&qwXD{o z>xt=zlpgEaYv_QSEF5?!ewxtXakiP_(3O_?Kv6|&FRSO!Yb#@-Z++##uf&(Myixx- zeLR7>(tY#p{@Gn+k*IVG)}4pOpgBfwMDE{dyk8l@XfiE+>^6|}akBwe*9aCF(Zc1} zJ!N%EVIJVVEpQB}myC&dmDBzHg&>8@SZ{QSH__vmUihOye6fIlf3|WLO5{8!I4mqg zxf*E>h1BlC9>EF;bt*^yn-2_vdscx^35*wD?H9cFH$)Wl=0`jPIhJ6@kB%hYj?E6U zx=G?Bmha1eYxFi%eZ02R5P^P7jD4hCxYWy+*redsV_27ES)Z}rQMtd%4|F5-?WieV z!GAemXt{}E*S(5Bakj**9v*vO)Xk1k()oo+O(8JkH6vx3Il2M%N=w<)1l<$yVY+yz zFt-<>;HpllbtUhpMo^weai|!s3y=eDLi_DrJ%!}a%j6~*EvAAR{WyosSN&6bC0AUq z?qSg+)s8(Xr7T5}mXID*1up_l5)^}=z_h^RC=9+&*ohX*x_JQo36?OebCZ1$$Idm& z22FI_!jo9~{Kgrk)+Ca-b6EzcHH~Cx)&yxkgKRls4hNy$Vca}yr4O$OH$&repU-NA zr#n$P)#AtL`S3P$p7zl48~PbLm_#(&P` z4W--KW3wZT?p`P(aOP{^5=F(oClrevvV=dJ1BexMvCAQjEo`jh&{QmNH8`KT-y~G& zI^~s@)EvRA>D7$wPx*%p;*Zv~tshu%P0m%RAFgV$fI~(Zhd@pnVmKY#Hhrp>oPW{f$|=)k$vu_-wRkaXl}Woo?c(J0c8|@% zkpnYg&xgRWNf>+vjB9eGtwrh>iTQ<6Dz(y|rGfep|H9R+T!|bxEnKBW7*4Y&<U7V;9&yj2N$)UBQRM1A_@@{HXrVk%2!! z32Doul4xF>jM6A%Xm+1IG`3(Q4_=%|!#{94`GyE@j}^rsW!5Jb{>@avvwk6-86y-i zMZ8``>=FMV%gLYsZ}PExBd#9%?k2_dvq9si$t%NHS09EX$xZdA2@M98q73n`ld6eP zqKf7;k<=_Su%PJRGgcIrPJ7^K-cbhPKhneQQ>psVb$LUB-w%GDz?A_ZO`A$q5VD{u zpHRgPkq)3xBRY42<%NXyozy6s8)#soaJhVxh>ApjN@;sQRpDQiAG>|8$L0+6^s3C; zDA5~kBx-jSqC1r~Tk`g*LhMwJT!9r&VTTm8HrI_$VhcW^%j(|l4{j`Ra|4My?pqTU z^DP?BHfmEVNDD$jm82wA@@EMT$RemCFyu7x=fQA9qL?VlvXzsyr8lAupsHTlIMGzR;cy}eu!{iv!( zXJ*_XNxK#7RWN!o%K)rs8 zZ52IP7a0i+C0E%O2 z!)M%+%C3~JXjY3(-m?BS7P@r7rUyP0!S%c+4S@1={DmDqkLer7@&4Fsew>j|ap+UJ zB|pZX^ju-D$C*C)c-eOyi8GnOiQIpW>wKWPr6=beE|u_8 zlyrmIp+)w)U0S^!WAh}$zVKQ{oIv=Ywu`?@M#jg0DCfZ%*o0I9vw-(IWa zS@Ro+nWkBWzicuW`AW*V%3?`3Bi7`@VZ^G5zhV|>vD_inr5$iB8|BQd&a$YoUs2_f zR6Rb>TUzH9Ic4a?tTWSEb8jHECh@qs3uz)-5uAJhO~FQzpK{?wryfHT_f$-KZCcec zAozeFHKoF4)?{)FgIo!1+_#}x9(!3iX=tIgwYS);LmoFk@)JTCsJS&_!O>r9*RFK^ zz^S71xA;gq8~~n({tjamBe<;+|5l1a%AJ`!=R$|5BT+5@&j|a8fYM21F+k^N+#1}H zBu@}W;-4ZcGEk;0Qa0+IA1+>Va>hajoWX5SXSxid9fjk{2eaLB6hkE9F zV{^oC@zs3Sdz{2q8ffx!=ZL_Ir+X*anx-YnO1NHx<)8CY@kOF2h8rLQhtV4&oVw8} zKTEhGjkGp=RMiuf^|P1sUn1>>4B^1c{b1+@*5sY=n~O7`zx>6e^=`0IXExW}ueKG1 z3}*bh^`BdsP2;ivtUDlC?n#n^U({m^;WkWYo6o4=C3&n4WBN3l+~clvPlp{t$)^Nc zr{Zlu_nfxC9CslOs~=*}De~0XZoK@4NNU#kTo*GZZX`DBHdzxU>~t&YAG`jMM=B-69xDM~}-mh?BX&SU&@m#VwKfp2z|& zl+LH|TqZ9+TXXZ8$8C`E{*wZH$oBUe2cPw3=qw!0AuY$r9VS7YR?DcP`VZ33F1Y+d zT7gv;C9=@uTGr)qCs}ZFNB)_EBm=bP1&h`WX4!A@4Rp8~)b`-&c6?1oz~`=$^|g_( z`-gj@G`|Tvw#rSuV20es5Fu;C{XOFAQSt1i-dg)bYgd7_(fUn8V=WF2krzh@a~r$w&b5@#YnCAoDNjI(rdis1=(x;;ZEXA*+Y$PWGfj&9KVxR zLg;Zw2zexdUP#XTV;=T3wWWUz6S!?dXG=gEK&@#qU19pdrLI0Z8HCc{pb&mg1OaAw zK>VP!Tbw+*PDp#gJ+;dEa!ju^gO?RFCR-+r#u%2c!1Xd5^)>rT4@2u3xsHf}qe$V_&bTkjiK&kw9jn*dgzk2}#;hNDJv z^=rHYGP>Zd&E1zBOy*tg$4mO3rQu9rwBrwA~8Zff4Ky zNqLh_ue4=Zwzlh39(;{qtOE82pGxkpSS<+dFT@XE*7sN(oF;Bi4of=T@&FCe?wa5Z zUPXKY;1u(F4Y}pZX`l>=M&sf}2yVO^6`99_Dfj@%){ zb07EDnKKH;;sRj*0(^iah`cHQUtH5jt&2mR%|2gxo1u~S#nY09nGYME64s#W09$zt zqz*%uOF|LNc%oJL0^4Jl2tna$uxtOAzl~+_TDrDFsQCDufYw&5NJ7Q z&-@|Q0O+4nNXEyI=3kvc{^mgZSA8DBWTITrIoiP6{bFlo^s*!5-O}kAAB;Qr_hfOwg zwJhI5QwEHFHyn~+lRu5@fM8Mxb4a$9D`+iDxZM7Is>?PzC1dNj2H9xq)wQ);uIEc< z>$N(&X39V1xObR5F$6HrFE0S+DMEnHCTrhUQ#R~pt7 z|5N4823-bKzuK#`Wv<~`+r|zFR)*?-G+IHl6O9IROzB0gq2V6^7 z0|`QU2sA822<3N|fLQi^R#lULq~BwOyBm!Q$>ykFX0bGK>1df$rwS<%%R?BN8YS9= z#2SRNvj5J^-I5g|a9F8QIfJcSN46}0&MjfkB*~NoaS})&Ds}55dE0lkjvvTc_BG&{ zr()W$Fnrkt6zF%LHbQ=iAckm0sclLxZPcAOMb30emo<7Y)&ga!5cx&dN_qm*um=qN zz5zedEYzD)$1f;Gx+(+=McJJrYx z)4bGFnNUL~Ab(O7@asMy3-nn9q1qG{bWi~vH~rjp*&E=B*RV9`HBt=3Fwu|!qW9&LKpo9oxBn^Lbj?n>%?t&O`D;KSrx2=4mXvQ`|vH%Pq zy7b#iID$NN9foQtP>YAMtPnUWh{29vZ{%4x$X);l@s)I4Ky>Dhl^lke_;UFKBAIin zRFq7HGv(w-=h${iq?%%yNfEcG&`QAh2RS23pKgzDFdLP6Q0UHqt0L5fhY z>=xCoyz+hvoYmNco#l%%kTbt$OHCw*A#nSnQo7qW_v2#-?)rBQOT%LT1o7+JLN`zG z(typLR`F4UQChasVmISZYR?fQ@PUBh8fZB@l~mvR)V{1SjZhc&n+NJe`>zv)mtf|* zwS<3pf}k*eB!eSW3f&>$9YF8>k*^2cU$#(-j@iEm2Xm^ju~2(G0=}6&+{3ZgXfFc{ zFg<9Dz2Y`455stY;}_2?Jce(t!V*iUt81A(WTC0J!m_TZ>*k(HTSe^Vh_{qM5-{&P zHFJRBBrfpgqjc!jU=vE&1)*Kgd}+rt)0ge8&(|EcMf%PgRWZ4yVu!XTsBWAif}Pk| z*Ln^N@0ooB@z>(S*nNW+9$dYPmc^tAo zSiC})&f!;WH?Tj4jOy9SUQn&>SSzW`p_mr>M3gh6OGgB{ihjMU)h*bWU0lM96=P>N zOr4`F95ZdgLyFRF(h#yF>Gih1Klwz8WV_w*I{kzL8My|JNgLRYT2%opAp|f>ByMAd zAg6t^kRJXHYOxeCSoEz{RAs|x`eIr!yhO~=F+IKi{G9IH82ntLPUjp~6JTXCY7si-kra)KF*8|6%T|JI zM`<-6n-IS-`H4DEL#D_Y>QzB8idh2*PT>Rd`@DiMY%}A4U`0`e@f7{l4xqCbo9kT( zw&`go=6W&in1OJp>8dN7}FY%93KXKT3bur;hVu9@?rY}HO zRdaiJ6%^awPMNT&=V-cyg!w@z{jn*sA2>|GiI7-NQ$VD$wFkLYAGW0ltP$Ts$_nEg zwu7*KgLk8O-rC_Xy>}%R3+(*EFXxb7 zpjDY?A5c~)PfSjtN&uHwS`4)*xq+sN<&w8$QSWrH^l%#RjW=bNtF~#Y=rLp)!B4a8 zmtsn}PL77dA~Z^T*2}YNo@dhIx2M)1{}?Ez_>c3u@Bcw4n?4oZqFJzP!XWtf+JT5fj$K!7@QIwmy?`K0h0t@nYFon74p z;*B4YHFNw{<~gZGF3vL+pLv1=!Z`HtsApg(Lk3tErH#~^f^kO?PC%an zs(lqufTPJ68&SWR2?yQn7)-QaH4Djpuk3GAyTbKp9->MeC4;;S0py9Sd4^|;L%kV> zJmfYYr&SeDONrTTO?i470kCz}y;ORHF<0qr9I|qy@DcVpb#AaAi|aLPIuYz-Elqqm zJPF4!H9RE$_;py88SRI;q(a1PRd*4>JZ$O$kbjqvSzjTc6%+hL?P8==YDj1PEM zcR@;E;rIG=F&y4>aY6qeTZb9D*3Wl1%1i?CrcpJ^ z6-b|E*}mUJuurN8A1_xq7r|nuBv>3i0wG6Pr02Ib%an{-BB=F92=7(<9DiLT+UI>Iw0kh)Am;-o6fSPU#@rD6kh&|Eg z2cbIhH(<>BOn(0z?ykYIH~oG^ZOMuj1pUDVf2?WZYz9(A3$%KM=OL~;mQ6yi@hW#_pK zi+3nA@hE~ITY_z)MD@;d;(|HN1+M?XclegqXgvFc7B8t_jxnQe&R z_w@BTtVKn`kOECkC#dio&S|yb+^b^D?Ti_UyzR?osF;E*`%kP1 z6KyAo1LvC&CMS9VZWI~~ApCe*d6ML;#+KbU?k<{8`~1upMgDWelB|9Vma0!2zf^YU z-hsoaiS>Ri_BGVN@9}*pSsg`7%qQ@33#r-2blp|+K0U6Ghec5MH@?A(HjgcTI}gw9 zy+>~o=(}V<00mlG5^d>PRvUJ8cQa~Lo>7XDsFR5j^|-bQ?!}lS_q%F3sQsu9@a$ZG zd9(7tZ7|9v;%?qVF6HZ={J6N;6h~A2zX$3(q$Jh?oE!hB#SHL1%xt)w`w}^H_I`AFby*`yN31N$ z?n>)h!`csei7`BpU;Un(T}d_vUkxk{k{MHVMFEm|oh8!SH{;@Jl|jxWjG`NVR7n>r ztH|_FhUT4e<7OE3`?>9U=B|8%X%3q`r=hk;QRO$=F6cUVi;32)ZBieDs0Lz=g&4-f zO?C{&jmwxcD<`P#&&0;hSe!kP1O7;X+&OsT)sD&ow4?d?L#cHDf*8;tQ>`R7k`R_SL=7!<8zL&+nCGXsmZ@?W$+_(f-L%ee zXUv&c%_Ih848tI(E!BoSHd=ByBQZ?n;226lX6PTFI)*K_gdAI#grG9**P%0x3$Rlh z2ojdIJ=y_yDWC(AK;50%vlZbbS9#CRQjt*g)RX~q}NdeW>f>PaBYd3sAg07G%~cN}(!I)ND9vyO`rCeqoCgwu}~jt>TBaZ!;2L1;*=^DL~Y&A%8qm z9pSlBtbFC%kI|IQnV#f7168Mti`{MU4|o_N0`wEvX)Z-24Sl5uIKkJt14#AeRmHGN zbRd%Seyd71q#<7>_PLB>Qq7|zIxJ>cf1hMl-9CZJ*6<%f!CPK06)J4pM8ovps+5l2 zaSig@3x#?PVRvv8>y&{q;&5eJJL|)g3{ju+)^30%F{gY7(T%b4yOYR(Ky4v*u(-ka zKSic93s)Kl@+nwx>CY2dDXy8I$fjc+)!M!aGIt>}uSfoRH!kkf;XRq${M3J+?DgG> z?{3ItH@VWwD9)0tCb(kwiPe|Ge)ZyUh7V6N1Xc0bPPCRQKA1b7QXt&X>W-neZP;#u z)i?40`V;@r!~jB1A-qJQEaZp#N-Be$oL6l31`nPRQu*&Ak}Jt2>cGl=+$gU(K1;O0 z!|!AwINc%wMyqaa^C`Kv-c3*Bco~0mqgmcw(*ce>B_cXVPRAW=P&POsXz<2A!|YEV z0^U@Q+pczt&5HF<8s4hKiG?X{JO*u1sf@*CFO1@PzT5b+?z1G}j9bQ;h2i zRhHxDq}Xd|+@sv$7%f1LuPGUo#CW^}{!Z%AwhGL2pQBd?yWcHFidpI<^Mm#{$0?JVWQL zoTgsZyZJAXQJ|F5evhObzQGKvnqY|b0t(e0nJiI+;*PzPDOnYKgwFZ7biUPQ5>&B$ zU5D*@l8Jka;%DLdUefWUaP!1;F%+h`YGq0y^MibLU6B)2V)u!#uqk?W;>t2?|2k=T zozXOu4LQruFOkljZlsgA6|lr23{G&o6~0Dib0$sC)Qq&MpQ~G+a^D(ROyu)avOv$m zOkv;t1Q$Ap^lZK}))GPVE}IhubXF!2dU|`d|3WnY80D7t{3{yvPnEiV(GmN<2?76I znJ>%V<5Dbue-i@IO8*g;QUhuHtCjz$o%TOC+&|J$|0xIjBR};&Lk{?V$Kn2+tNkC< zzW*7j`VSEIuh|>xU$eJ=op}2Pdt+r`2Sqt5JkH(aJ0Z&2a=NIYPmdlIHF1;q$hUc71u2 ziGBjNOKV}<)=z!;wZO{i>FMh69ox;Zr@OuL6{+ztH#FvI^>KLF36p6&Ei3vDdYekK zZtbU@PMf7c)`Wo;_D&3?i9~Ak>DKt|4<42-IOHC9@J?{3ns8eRS9~B7CEz?B)R*?% z4Kc45QTn1@a$xY92TfcE5la^hJ##BakurZ3JjBPaZy}07)yEuq;zeKcQf5Ht*$a&p zRd*xqml|Qp!nT6T%*RxEi%^ha%%SKf8`QIA;o5cm{X6xaECD6hoo@DNu5$aRledbnoXr z1PA~<@*o%Oz@L;qk|mU&B+6zgdX~SA&kM+0M`qxet1|~nJaMrVG0F#LxztCJCVoX9 z>-0X3Qo6KhJj+l)ShZxu@U6rX4AazY^{6?AIXwu1%Ts4%!h{&+4oiasaheHH3#$V% z4U2s%KyN}H2vvxQ6-5Z>9=H56KdEQsgD!|jE+Ax4bcvd@Z%cQf({G7 z-cYZnCHg0x#I$v%&LGuK0K5;t*ah(S80_-fh4*BQI0e`z_Ga0(nQ{1T3Xpwi!3Y_A z)!W7h6m1E@+%k&O@tb|@Gey5m<|fKD6Bz4nt7g@SuL)1&f3s4iTctOa53 z-!MysMu$g<--!!r;9CGsI@NtDw{h)(2q+#O_7~fI2P>C_g18o!cull^2r%(6*HX(ge15zXUG7 zFlk`?!|`u^2Vd3L2B7^Ie70(KS?{Rpl2%h`hiYrHWItw8uLB;!C!C3pfsb$*?mNV4 zzK4;~SXd}H25+}=+L8=>2ED?IS4Nouy6YaOj0tfd4R57>J zeE8=0Rxi`mYLtDSqEg!fWZZ9Po;Lf=ohwAzz~TkLXOgNBP1cojuupZ;#yMfb=YoKC zmOuuqN>d$X?|8(jk}-Py!fa z0a1w4`=1zIIp}Z1-^%8VOm^Y5~_Qj~G&>-dHz(NJ#0*K_% zpT-ud)F9S!8a}zG`8KxWsrI<#KG-aiPndIww~aAFM}eWd`Nhb<3Xz}TTeKtF$B1vs zRp)j->JGKjG3~%dv~j?RZb2p&xPE8-V8`P=tTW(!KyNuWi*(FAseqIu5KVej15E+V z_((i7cMPf`vS|86Z6e&^uA*v4_asilFP%gze3<#$MfV`J2Bg@PLRaj#`AQ8T?7d~F z(K`|)?E83QMP&(#i>z8CXIBYs|BMSFm2z;QYgDhHw`}^+qxrIVhCvDws*UYUA#|;d zP{#oR3e9TRoWtOe8YbDk4dm757x#ruhvwMrOF9>g5$p)ap%m;o#H{}HNUsPL_|mSq z0SbCK0@Xp4S`h)h_|RzQvGE*P3=6_|NX;U7xjI(&EmoO9-X7Zb_j0=6(QixZX#G+K z@G(cbk_X-hC$i;yegWfic8IM6ZgMye#|2kCj~>qfVbtb>HBs9dzK-d~rG>Px2v7OJ zq1TuDqjYC*;+(~Iv;t2-{lf=JMC{QQeM7SBf&&GM?b;1))DI|45E9{_vGYP@6=tA$ z>T)yT0-QYXlNrL*{=HMJjR~tIVQ4WKAX0#N&Bf8}kVeosPG0QC2O1dmwK7-mfkc$i zQKPBpqIovx-&N8pA0F87X(fkYcZ_$qq`$AAPKKL(@#_H4$f84A0FLMk#;mt7FYGSy zjO>+GC1tFBCzbXl`vepKXu=qGFdz*HqyUX!XRrxa_R|k;i4aMHhjMc0wYAyw>X!Ny zWkaZ$O%!xCku3s_>)E+QQ|DDdRF zG@+FNl;eJJjoFp|!X%pIMRc=nc5}Blu#^=Q5C&~NSyBhDwK=yV!o7yP!4d-|#62SS z%da3;R|tJP7Voz#th?24iJZN@6_wv|lB;?&o<2URmq6hW4j<_!U@XYy+k3>%&-8~z z+1k;Ty{b^^6VnbfgYy+l*_F5z=1RQLGlRS#nG7`eP2&!}AM}o|VEHgM5ej_GV(rde z0Y3rDIwuGDdS;|#T-_d=!55^#a&9o%go3arK~?)AS$!y-$PjlYnm&u1^otm%%yWdO zo?MXEW4wM?Bap2w=nY~|>C=jx6_JG^8V?%QrENqaf1ue|({U+6Z*id#Yh$z!HH1X~ zx3ZM)V44B@w)D7s_PBH_^|A9ohjj3Zg>bv0nqgl|WP4J-)pNUteB0Ys7oF2AfFSRo z(yJJ!iz|{#uS3wP8@5rc**BmH4HN5^_>7&SAU<}3#GyrNwExI)MBO(ZXDhMbvjw=| zuK)bPl4Z+g-C1Plq~pas+;>`a4mPA(Yf_+jL7aIAKM}Mg14;!a-cTG>ZWs|2R7Ms= za>R_C#DG2qU!}Zy6fk#sCv{3i2C8#i3zz6tsl%&-PJxv>;B*mz#fq=g>O*lCKFLO~Pb ze*U{+CNkUBqp3q0|e>@H#h7U&YB64q|W|Jyhv^DY+Z}Za4}LQw zMDw?5P%9TsN}Ns~F9Ge>{7+G$v_#_%X|Wx+r~JBL%xQZ#9%YWK!iyi8aTt9C(mLF{ z403xH7zNEd$9O>79x_BRwfoP^2aY<}%`%eL}irxFq^@wMC5X^3M^S0hfSrHqsbZp?c$6 z)SB9|AfNX9r_}VP&}$ZU_>$y~+6qr(Z2X3@Ml+QJIT^Je<)&yocX4K!0RE9G-fJnOM4^vE*bMoz@H$|4{R03~`mq>i1=3`LPyQfN>EGGr&$! zYUttX(?0bj?!Elj8mlw^r<#dxAwV{Pnb;3SG|(;~A2ew|qDh{vK0#DdO&v{FGH&(T z?}pbsY!Fi^rO5)na1M!cu;Y2_p0VibIOZ)&H2Lf8it%5%gwF zjm&rWm)AZ&?rZDaXDWUsB*H@G%;gR&QJuhD>bQFwa54*vc>EpzU}2heE3mEPE&gUG zcHvJCS}aj0y-zH7hAD7K_lI}NpHf}4xkX<)5d@3OG*|=xU!W+|ZmwP8Y21d1>8R}< zoAdajn!1;F*nFb*a!G10u;ZAMT?eFKAH4j*zMsmfQ__)oCKD26-FmP#J7cCK7P%SC zMMc$gp5h>xtTbPrs=8)Yw?=M9QJIZ#xX>j=24A|~CM&^n9T*C3L%dO}Do;n$bYPgx zQj&}?=$wb~g?$T-Nckif5u~ciDZZ_y0k^-CH-Vqi;NZ3IQhY>(jxW-$0_*Ztyaj}K zlm&@e%EKSUNY>EzBfS7*z6OLUNUWs3}}C>;(yjz(&NkjwVsT#fA+VP@GO~5J4z!3?tE` zX49*JQxm*>mdR*@yuB&~)OZGEE2m_Ry~L7n*NS+msLXwzTRTfs+%@{^F2WxJQS7du zHDS2S-xKMW4)b1D@11V!)q^*$z=j37YJagW3a%Of-v&-NNSnT5(hC~b zAvZLVHX3azqZ}>MgaJsL?##~y>2a)P?B~Cos;xKPs;e8m21t#$Gg)p3Cb$I}AMf`c z3T8$I+V|jE7=x`%>)unYrWF~B^ZKp{@gY~6WWG}HlR1E-ki9DDk{1R+j}#8ZU`{ro z3-6LNceAG@f_m$|wB97sk!eV3KW6u-)e|7!OaH8%@q#F+%wE>)DfhArRCx-6Sbmv- zMLn9Iyxhke$o9Mfx~A3y;+f`p&)BOa#q%(}7yf1Qx*AUX1OsHHw?zH>^)9!#`Tb?g z)yxGUGfXdVY#E`;iU^EIoo{q0a$u0eqLsD=olo(k;7-i zJgeTODd*}vwenn?IfiXVg>$-4o?80?z?6p55vZJL86?uC-ZO)H(OxCWU_m7@ULPFDFLgDPgjdHh>no7~I zu^q;M*Lw8xGcq23aT(?WoGZ)FJkw%x{Bp6Mlz9}zqta&gGjwT1toXP| z?EK*J0Mk>6q$=~F`LD)WH5$7e&T#A(d{@3E!O!5ZA2I%xBVn9eAGls~Kaq++v%5#8 zRloIFFl1$pjBU>&TEE71S7DUE6f6GkMbn%iBM3q##-t2PIkONjz$lQo} z0YJVriYm76(i9_MfN4}8=bjI`+H3-6cxU!lE9>BV_!}sa|EUd2DaX#`-Cuj=rrTCi zldj+5iGARZa%Gezs-$cReWb4*s~?Yn4@0q~rt@5w5}^rvF0l25Kk*2*FU7=@mXm>C za3Q!Q^~usO{z@w;%V-&82;)-^OF?o!jDS*Po$%V&M!#Opt|@?|z1(@N$7c`rW(aHR zzS=E^YZoAs+>px+RyVP_j+M_@7WGZrC^l3hY<^RO7RJfOT(wK(lNN1dNDZOF0%jG3 z6+H`6M7fHL2TjSv>vFSkKF0%D9sisZ^`@F4)EEWpizDH;4_a?Rc^QC6{WQmFF^v|w zHDx`wNP%eE6?Y@0whkZRAM_gbLTwGa$mn7_#e}o~LtMaW4RfMj831)JS@=QMv*Hns zW_NDCNVao#FR%N)VlQnrWBV@nd4Lvut8m0Bs>4T7_nOXAciuN~z410UMPs!1175}) zq>WE@2k=ab2h_Ylb+!`YK-Ouw7{6mPkFvlI=(&*lR*z?R8_Mj;1iW(-Uz1NmQ~+Y0 z_JR;P8(hnTgBZvb3^p9mRk&oENsFk?ZRGG2yw2r$P4G-Fu)_jA^=vreE{X}Sky?YH zxKFgRV0C$qv!7i{5#GQWe2|eNBlK61FLG>puFRs^b* zZ4T%Y?ED%r`Eqnb_`pQGL)nDL4=(Cz@9pavb_@@*y4M$MUTGXZP83y28cdnNX;G6>`~keL{mPfKtNVa*>XrZ)5bZex9lerc1Xq(wlF- z#tENN#`lmb-O<2s)PB-<2CdE>j}LG>^)*q8t}P*kH1Aq)7Hpsbd(+Cvdpls=Rny%^ zbH8--{1o?%Xi?kk=-=N-!>0#*^fT@dF|T2C`_IGCZHO9ImN0o@@bJjl=-R?!Pj z@9jv!d(v^vuMr90FLgC?zosf`>C0_i^-@Wp7S2ai+0s-v&YUtZ&+-$pArdx2%IXmAkPRn%0C_ridM-xQxJE(0r)*UO}MQ}d7#?wBp`tNVt1 zH&agop;OIG4e3Zyh`qy|J^3@`PeC17alWpu}*$Lg5m_VGez++_M1c_ctJ)zsgy&(}T;k#aW*l z@0)AWuZ2qfiunmlICe9|h74zFYH4o$!*W;JU7k7XOKeOkxG(E3)rDCFAJ|>)_5E?AJKyYjJO-Cbm8@Q`$y@EniPQYuE56ZxK zk$e!K;>Y-Tjzeb5X5i*o1pe?hL9|EcKf zzh~ww4F5Vr$MW|O9m~Ik=+e*>HjrerF)#!y6B_e10sw#kHq{_G|LP+ChRXk;vHoY6 zj`?r-zW*`a>i=_9_3t%e{|6!ecXa+Qu{iTzr|kaiQT+EQJ0{lu(l3^xZfn2Ef%J8) z|I=O^mq?4`(My9Sp_p<*G#c71bP({mFoE{87R59XYLvtXqI97M z2$i(qI{xhBV!ZJ134Uc^b?zEZS=p#jQ&se0ZN++B-%d}Lc~h4UTi-XaK@~AH26yfG zI?pUHnPzV;C=rBFT3tpw@$?;fS|m+aXdtEup!7h<@va&47d`z!D*7F{{O_Xl-#O{G zrB8pTIF!8ecsKw3t{LD4^c>1}p~~g)_YYAbn0X7m8ID0n9@%*=3<(`<2vo}^lb^`5 z@SN|LuSw|gxpgmA+)Op0at~`p?#VAx#X|hRl;UnGyJ2Wb@v&0o%#6QAn3~z}u>}r? z7qJW^0n;*|LqZ6mUPqD{&0kJ_uP7(+fAMi~BXXsE9umkbgg_1&3zN#0DD}&)#6M+BYoN!}{yx z8p16AMc|L4G7_@@X9^jz*(f`Hu&QE`2_^Wr;JTmnTOK_s@9uAJa8}KY0MKns^Wv&f z3&ti6D^cPe#^>q8^bqh2zUa^XDm?LHWxQdBzn%cx)K&r{JRZ(x5|Mbi zQkqjm{nUL`WRc83QwP$5pY&<_B1T_OzJUcPMhxI7>H?3r){e?%P0}>hpnl4@kc(h84K#g)Q0WlJEmZscDv(I%2Zu?!3vgfBDCeNK`sD_Dgp#RqHrR$8 z{~EZ9&XveJAM`8=!Sl3{2_t3Q%>l~%wg!dw3Ok6AWW>eKA4Gi1oU4i6?z6v%;Ig;R zOSW=`$2EFs8%8&Ui$1_7SC(G6h#qegNb3v(aM4LCWz;9ikRWSfoY%I227=#b5qKHZ z$c9fWlIzOHpFvwS>hS5Xn!t~7G@vsWOlc$DB{@>T86{#tx6;fIYf*{_gvO+Iu_L_t z13Jr4+#-w|XmUCjG$NQjp0%W+$D_Y_4*rEl*}2YYr__0+8Rj8lWpy4+A8*Aif0vvvdc*>x5q23z|DH=FRD?9Hch4DLvL z%^}DJm&Tr7XL$E!Gn85u;F|u>0sO_qBP&4Hgvd6Z&zmQ2RwTUFzEUayQ!ux5#@Pa>J<3?d_nyBXdUj zDn*w%imw!fmE|^W#)^6CLAmwxTNnhVhTDf2NZ^Rben$@LoF+Z^k-zR)c)h6m^@N#F zz%%}7rFaHK-?32B-b>)r$OGt*mYehCyG3#tjBPVh)chB&cgX?9L&LofIvEitlnK*}{CNYlavw~udx*yrnTEQ1&m*u} zKShWYeusc&EksLaxFu{aihEw7wb0J!`XQ)x5CCllynP=|)eY*7p50fZ2(1nSzk7c~x`RbWO4A17-8cZn~^k$0x=^F)c@6& zV%)fkoBap-K@}Xj?B7j{|AJrm55vX(Jum);;ezSEk;d>(*zErSKJ9-DlPv#|Z(=3* z&sOFCV%F&gf1&JQZ$c+;U}nP0`;Qa-$2iIG&m;QZ4wlU9Oicgkx2RInu=~k6{eWxF zMo)+=pZiY$VZg$&AsL~KSHzj$iQvJagmIxmm&BdBzMi#Irfu<%Onz|z3MyS*|KaSZ zjk#zIHEpF$fqg$Oy=awDqHaBops6{1%=vWiaZ>tDrD>4H7*;G4F-=<*CE<{=p)8tV zT6=iCmAdbuTYn5~P)`PT>NHVfyRW?1oK2vKm1cC+R#jWQ zZ#RyZUR6^RPp^ATn7)~=dqw^SwN~_8*KvG--Q>Pj4f^Zv*d1fUE8)Ti#<4egaaRpd zF^3WnqjvpS>?>a_$wVS~#I9Ag@cp;82I45Wb5F+s44-&@wFO_%bzpKKuHv7(bWw+H zA?PoL3<_PGg7lNgt2131B-Geo7gMrd1I$>2$yk%;lk|d^Oh^?0Qz*}w*E^uSmHjGm z@eF&7Fy1Nl{$E7jq~6HoK}_21Uf34He|e*?WrHP@zklkK9sgOUOo=}F1Fls)0}V7q zy5Sg^&i*oXrvdsB6eZYU3zKxNsxr`+Jb~pvh55h#Ezp>{_@y6f z6u(~p22gr^gxz|+Dvqf;e}Uu*33jswPo9K#iA-5HxN4s($O(kKg*8!<`;}n#(ts31 z2TR-Fqug(RXvL}|Sd2c-=A0`_^R^)wgv|jKHR)at$e(J1#eSTHrunv6T@cx!y}#7o zjp^e|Cl{ybY{*k?Nl$%ZM#nd>^ADE65&^&&PSw|=xS)CO`yFa)s~|BkAc;_KnJ~#ZflZE) zaKb~iQnm}Nf%ja$O2uhKn+pq8O&2)IHrD&n@y?u=1WX3Ix`uE=m}yFEB2$|!h)EKr z*M7a!okW7?^A7)$fSvQr6b`u!&J#*ozGnSw^`;>KVM2>0>R2^!#|`E#C!sSUNbJDF zJBF3ml5~GhevMKNY>JQsmd6t-?Ywj(n=ZGe8E_yC z$LCbEyam7``9pQ*=H(TKnJ<0{$2o&rt0VJq3&xD=1`fIHTc z1}AgatFo_T(o{8W&0%eX6%hJK@Pbj23R8J`7QFd;5w3eNu*%wgDeEgi%_;YaD zJc1!6UrvCTlphT-^oUe%?&iB{)-t_&53AzE*Gp(>YpEL?rm+xaQHl-`Az<=zumdaGPV~UJO>F!~m#}d=zn$?9h_% z0lg!;`S)(bsC(qa_QP4&A25Vy_!qfxHu>}kCTnW4uJjL>aRUi1S$u09yzbWm|BMM1 z5MQZe!U7JXI`QxaCrkFzgXqlTHgnNztide{N5;ziK6(CQY4&mh&JsG%Y@~WC6QG~Nrk$b%e2cgBQ zJbIdAUt_&?5jl-3D6{+-9K$_|&f%I_^Bl?QGMkM&ep+KqWMEF|grd!zze4yE$DG(Q zlN$rw9RNSem-+(Yve!;r`_Vpn`t-^^QNd#pJ$B_cAg?l836nqNP2gH|I|$V+?QQ>ZSPQPVUP@KIDFC;2PPW{P zwk$cY#c;$I&Ip<@!Lzqu(j_IVD)&;rd@gB4FqWVt>1z#uH|6Saeigrs9z?<% zDHy1sfsf%)`kc<|OBY>*J^drN&fU2rT^!Bi`yV--Y!682uYMD~FsFI6C)v~fY7aY% z_-+H^G#{B>?Om~WPKtS4YZwyt?MPp2RvHoW$9t*8_>cM9u@Qh3V{W{2wP-QRq0Alk z2e|hfJ94|wyObAEb|a^UzxmZpFAmz4HoRq9W}%rqJKE#ta6-ZDh#Q2gz{+Jgh%eJR z#j>WeIZ;x?2`cI0THoEp)|+&+W?DDpssQc4O*@Kecl0%W=wVS31+?0hd3;sV&o`bK zBrpre1qlV5kaVoHv~A!S`a;lnKLUXdp#pU~MAreSo%B_901uKN5e zoe0aH52uoO+5Tu}SXN65s_Gev^smErJMp7h{&`bZg0CRD^0EhoaD`Wq5Qe_>j3>gQ?cV5}!RQBay1q@X2#~jKqYyud^3D=|yr7Iye z!d*#MvGuR@dNJ@QteKLwqGO8G5dX`T4y-_w~p2+?3-s(!{ptpoxy$R0@QAFuZ z7ZS*O9n#YL$L261tpEZYV21R`U)>gg8=`UkJdeObp5WhXsJ@yEmSB{c6=m=<+80d3 z&l>bJPguTyB1so$2n;n&75J4Z0)u){9;5nZ)*ij>noqXF@Tjy3R5*dx;^~-=#J0(A zNS8KDzht8}V%evEfGoOpOvKDR9tWj~mjjojSFtm`3=d5btT~@kQjPc#Q9cx-t@D@9 zR-nsvt9x0kR7BDO9(-H%YE-sd$<+KOM*cq%d}EWpysMexZ?aLQvff)W^Rs~nNb6ry zIC7r(%$UYk;o%D&gJ$I)KtFZq-pOF^8IS|6k#7TY6^npIdN_2_HJbI%zR&<4_>U?J zn=OdJe?L*V8U1AJ(P$^8WdjBIVNk>a@RlN}DI2hYm1)My?=Rj{JcJzE)&%5uhFpa$ zQ=WUCX?hFgeaVmo8d0)6GG^w#uw9SE{eeg6BhP^$F;RrHzx+fH+Vl&0om3l?kJ&X3 zx_%GQ*qTS4q1A6Q>)oQdLt)e24x5A(HlY5>8EL9i{ab$;pNoNYQ*n@9Lg6*Qv5h=! zf`r-1&q*o(EMW&o>HqW%o_0xCU; zyU;LT5Sz8feZF|a@x#aAXf$4dGQbweSck@x2s~f_IYr; zXIQ)o)0|($k9X!6#>!ENRGI1s-T*$NpX|`~RF32UY$>R_S+Kfg<;T&)F2IIZz+QsigYO^ZWX4wzM8h2)7*rmz2 z?;h3~y)9QRJWb$Y>WF>_rPyu9cyy|*Q3B*o%29<=x{qt_E%~YOoEc)jZ~R?~*GW=1 zkhrdHwwt&79avW!b~DsZ@14VIBD}^9M<%@FltwM@;Vy|fU;)@dAxtLiv+@^JQuX3V zsk8&CJJPJOP>3USI zbf!pCk0l9YMoewmz-NUoFG1@&BkHTjVo+kbS-5w1hP@BP;&TeE>G5E2GAQGQoz56> zUOpM%I(fnZorN*KHjH+_c%lvU7|cElyybLzDed($$3W-6NPcG5x?Vw{ET28MX43F- zJ7|U3gpgUqr)|j&RBf}aFr7~nF)9P;0XF}?IrfBJsaH=hW7B_OPS}sjRKW;@gE^ND7Lwk~Io!Lji zx5_+j*(S=^6#Bh3ff|~P%R|xvI%zxWSaYpXQfQjyIj+_s-dbh57O9Togfa0F66bFL zWx5K#+<{W7#=J6HRH9xZP z^7c6wGh2D}O@iTYgNc0H;^3^4S?E4_)IP#O)NdC9x(?E7TU+d7sdWm2B*bVKg%yLv$Y$U$S#FMwm64U~pao4{#4g)YXn zp{=vZ50FafEH`~e4nnO)0-RJ=ii3Arb3&u~0> zDz5W*PTZACapq7Db)B;9(3KZ zez8NwqtC>d0|S(a19cAf5_vY4u-V%&)iKU-BtNQqI+qZt^*an6Zz;(3XT*6$9t@M}m4qXXwg?gi zMi=65iXrTiO$h>H&y7lyb~BCO_R#|iD~(|hV>iKHLAT&W@PZ3xlfs-`qOvQuvj=&@ zgjx#R&`%wni38xjpt~-Y081N7betXMqRx**#e;o!?$~=quVhZ60n+ueXy*DW-0T)M zt~{eN%O_bQUUPl{$Jq)vx_n?;zIuV6667@QumpEA?})aAYz~R=0Bf5v{L#=%E5Or6 zaLPH!0$&9Y0*hmDPHXK~aePCeoEg_^cPlJ+L?+VHVG%l(Q!B2O zN^Q;P;7xo}w^J_H8yizi(zk6@rCfbl$e%7-8}IKA++4Fs2FTH@h?@*#wf2+Ts9o#h zIA!!VLq{|3HhqU@d^Jrbm;JM8&Q?`gX52K=(Yc-uhF>e8S-JG4W@vfEUEljijNn^4 z*VhdR(+wj#(@_*XTiQ&Va4=h5CKTg6*T@G6lBru5w6LZg*q{GMThYNPm_-6m9b^c!15R3et%Bn*PrcGg*u?K3l*Z>C(_9~v638Go$jyXwb zY@OG>y+oL1>D`7A#x^iyN?Pj~!Vh0^mn&c5kGYasd+XCeZFn@*l%v}x9+HRY#e4?AjrY#fOo0cF3gI@UXgvC{`Rgd_nYo zB6e3@QsbPmI%-Q$BuNzK+&2tjaP7ZFpWnJMgnvl-&?+KL+w^0b@;A_T9%mzUOz~Ne z{SiO!fW1-O|3P%JMAz#N4xqxMh01fsk^Mqo?T$&>=fc_Wy%#4*3CvjL9krzKs z&kV}N7L{p~r($f9T$a-q$3~F3lZ8HMcLr-mr`sDaroixgDfr!ak|ZpNRBc{VWl}E< z+?*o-A)yOWT>{s;@<=_ufV^9*{YZ<0009fuiv(7?yKv!)1I7|R#8bce!6BlINdYQw zdoj4avR_dLiQaOlo|#WYVRma z*Dg^GRhqqlVersa@-p=hxt|rB2h;Do!iVDEg!rTeeX$WR^XQzAmVWCHgVHaFnef%PF*Ig{B zM@qWL`rd@}dB`yffY0fOyK$ZrhIQXwK`H8WSkf;2$S9gQMmwV9J0~c5s=6ZoASO8~ zLM?GFOtdJggpl&EWcpSCnYaV6l!t*L2|5~Ww7X?kwemxUq>j~ zyrEK`t%j*3LslY7n=M_7Y{iNm!PF1$98XU?!x7bpq=SWyYw#-{Y6? z>su#FA|Pr^79oJdP!{YLMpXQrr&nV!i$j48qhiJT?J>V7q%tP$@T4ke#~MexVL#l6 zI!m^pUPezzOx0C@GhO9RZ@2U9=buZU2Ou#&Damfoj+A~Xvsr9Ih%Nh- zNR7&QoTN$QnX+)E9m#Q2l9Z@Ws}j#t^i95vO&X9}>*P!1u6`Kg&ilkHWOz@!)(9>6 z7SZdT$gsDnh^rF}6B4k)KtQ(e$+*K$6^4frr&ioEqWc`4Gmj+n1UJ;ED_L_5#2aTY z#kI*gfQ6Vc*0n~S|3u&KelcT57|ZX{i$bN63Cg)*Wn+)csFH%SwGG*xt9V5g^?z>{0NqrIt$K1UXuy z5B*+R;0&0Bdeg)xMRCy$2=gE;D%G4qk!=x5mZ=23b5-vRM7vF1r@097NP(231&9aD zBY9LWoQ@)XxfU_dQi-j2sZ|yabAX~7-WsdYNGj)**vp0*xiU@JzRrF<^XjqNXSht% zryOl;AD9yJU8ur&o|zg=$iW6My#4p(HH@~Nck!AB=X!%)n` zWlB)q+Nkp`Xnk=)^F1;B7(4bg{yek_)@*O)|HHxr<` z#p+gh+F0af6lfj3PR<2({t!%#`caBkXvZaOxU0{LHMJL!p@={QN|wDHHMM<@ z7xQ%&OM+}XfY36(NVYc)XZzP+ws}EYOB3guukILU?JcEvbM9Qe{7{6a;2&$?No$Nv z$|&c{kKH(2oP26zo0?agZ!lSi1pPI|u2vVx15j0N zl6|XJEzB?UBZ2=gumM!X_>VyR?NeqDem->Pq3&lj&Y)A!(L5$q)zYh7CVHs5QO3X9oa#)Q_aPO*VA6aCIHCorh>gl=uWMi+}{Trwui-N1wO)y zizVckoV6#U^Z{!wW=;PSYVjnhc1Bb26b!6od01?y#=+m=RTC`VD@)R16b6`T#0r=Y zs!Bgxg;-R^uXYVLD0h7$KfySPXF(@tEAnBsj)QKUBlpsco&yo5Bao7P0LZjiCnQ0d zTun3mgRsvr(@2NeHuS`x4BA5pq^WnE1gtqU?!K3HAzaj=x#$RRY*MS-C`0pzSw&c0 z-Ul|_Fr+xOs#2`>*kYq370b->W?I!Rk0um$E6)}Jann=b${Zwm7nE>E_rbfAJ=4kO z9Zy^xj#O^DEXzvyUD$2uwb|l-qc<;xS;o>6o)F@~zgf)h=Q6h7s)wX6ZdeNWXIEGmshO!U{w3;fjJ*PKo7Q2 z?5i(q@8RL}bHyCE$u#FMlU1rB;vfg+ zbA)=qA?LwPT(DuViO0~+=*?tPTPs1x-|cmLS(rAE@+$Ts!Xmgeitk2U9EkY3Zun#i zAP=5`>!q;!1GJ`1H8d891>YONGD&)^Mye>S;T~>X@`N>RBb#uy8TdwBY1~oI?gLhe ziA1SA1SlK7Z(f_XNqgb%7SG9LIV-Xf_-~AbY8l%*0fmif-a*3!i}C{>-ByO_*g;(*Xex%4(y|74x01>cGr33l^6@8k(OG&>pjH4Y7gw67Fp0LSJUT^2@gWarmXaND!9I!)GYQ=9cvNlVAbtKMAU@nMkEyANw8sYY+R z4$VH`=+zZ)5A6D~sFmUf?R0?I3Qr=J{I-CYQ2vE|C^wavDSKH1z6%qRJk%7uOxJ)8 zL#Q4-EtO;*NPZQ8&fn`gqxq}kt36-K49FJ8AXjI-ZR{DRF7oTkSP_Y!zTJ6lW*&o3 z)^s=i?96DFPEA##ox%u~f6g#*UA>gm8>zH($644u5v+rt_t6>%BCZ0eIJB`{F|nCEIJcOyX#XX$2_hB z_rHWxY@1}HY|(031znqFQv~oAh%=E^S%|CbSr!l4^C%skDS!M+=CB>0ohC)$aj7v=nNJR13foY<{M_Nh^p7Df`i`*FK|hebP5hK+{LFJhE1$nLq4Na>_{d z-~`Cyo;DR|ZzXq9o;0;NX(D_42-iSayGW62mi1IQoTIy~!s~7l{^eP2qr*ZJOa3F6 zjwpvzG;$|TTf|J9yLF!!ca~*6M>(WB)AL~l9zNtVW$|v;(1Q#- zPjNNOGquaod&uQtQo`dkD4!6p8PaOC0`EROSdCx4%EY}MkAzQa$OC3_SQ&CM?I^Jb zm^oz>I=yZ8@SQWuo&Mv6A`1dNKjS%-tv=s!P&S`26deri$LUm7Ywmj+B;V$z2QGAp znD=^{QrPeqW6G#RDU8AgSUE8p2z)++r|y(fpgf{o5c&SE#oc2nvf5gQJVQZ`Em zzuUMfDy6dBHdh;$DW6)>r9}|23GH3CwdYp0nUva^SJ_Ve{f^I>26y}IYG7~#%fq4) zsoD6#ylE){b35Bwxn8&$-`(_boc+SawIfqjH-%AuhixSD&ZQ1Ev;a$_vf88cTl8Et zt+4U#Q#i~~WOIc_zTTZH9SBoRFY*wwcG5`ygrOfWp0KD{Orj)7GF360-#}RCW90_% z+IE=hgS&spdj!>3C$3(YV@ICA% z(L`B4n`33=6gkeMHx(5wNUd1&j%qeUnDXM~$o!`hN0qzIYtC&327DS43q^g~NqMsL znK^0T6=`eizJe*HR5uHC>MINNDJ(Dpg+42we08-^-1@7VQ)tuQJp(xLkxDL^^((88 zP>(Xq$xnxghZ&)YGk8ES0HX1Lz|25nd^Z!e(=f8%#qGx9OSNmm?nvK(D8{$`uo`39~49H*aK2 zpj+m&vukr&V`HF1yQv;L9Q@?pOX}HR1!^YEqmbea2(>!u>7XSMxJcgZY&*OaXxE$8 zVdtJU190Rw7shVwnvzVDc0;>eyf4oo(a^;C{`p@&eEA;>CYM$;92xRBN~U&HQQSiP zcvST5c-=S;mGHiP{sVllar*Sy(dJpyH0 z3MdKbC+WI^9c&6(^8BV;Kq&)6Pqu4N8gvl<(Uj!}aa7?Cuy!o92h#q%fA*qg4vs4K zLpT#Z2zD-*qizj@BshF5XAW87Pw5rEsay@zNG%Gf2z3$PDPi% zkfoh@3bQz-O#aN+&d7+ItxPKEz#dMZc6rV~kVV;74z|zS!Cz>zIf`HQs~cGPIJfyr zoKdFY*4R!fhRVv=?z@bwE`*WAaQOTvC-)>Fk(4or) z;L(p%SZJYOJEg-`GCkR&`5w>V71bmStQC)`N3TgjwUJInl{9HhyJ&d<1;4V;M=}QX zW>ix*9lD0A3#5XCj_k3MK&t}>bfb?xb>lQItKXS{ku&@;t-p3J>NJhk>E5Rf`Eby>wZe+2CO)Ax^uF#V! zW@v`UX~ynBL!bHc6>!Z3O>sWGuaM00^WyyT;yjTPdJv^awqD_`JXj%yB#S#P!g55w|LWjl&AQ37ye#(MJ+)Q zJMv6h-U|fw^>}YWeYK|TVH?^=MU7qd%k0aEWkwWq&+W;~3rnZ{^|CI{ z+a5|AlTAW6`E$gtB9T}=NziXfQ1LB>tN{4zZJ~8LMKd?!-nID8rm?!{rD-Fn-%@EK zkEM)f_+zheb9E1|pWDL7Qk)vJ z*T4NF>tA1};6n1N=@}#IBCmr=kfB(cmygx#kCUI%GqAu=HFbGNRgRmI+pr9;GlpP~ zfox+5(eYZPf*^JB`XC4gDto*Ys;XM9Yi)P#Ih=KF(4KViQ#di63MIjMXfEDyhL91xS04(|bL#)e11G47 zKU}vBbGV*+?9zJ`CRBm3B@`hXrq2b}-l(w*bCX2A?c;qD=~Ds71h6-cWo*LV?Nugh z*aFkt82~j7yx#5uCgZ<5XZh&Sn)?LNI~WC8t)uwNNjw9>OTBRv>7^$OK{7n6Su_Uu z$e_9LkfowW%#b5wzG>XNXq~SWIQ9tqlII78tQX0S8-B)oYg8|^Q-KgKLDK1ypL`!u zEUM+YHWobA3fyV=-3+UHd6s3VX2wdN3d4HRB0XO7_8TYVK6FIbM}dQV;X8aAVjn>w zU(BTFJ~~O`YXv@G#m-NFLngFzgK#QX7C2b;mVlWbId}<&3JCQ(ZqXl-FHFDqjQ=ty zQjl5;&S@DtpNrt=JpiC}Nw60N;#!2i4ELmhZP_kx?;(d^tpI^8NZ<(xW9iFrIn$pD zq0NVIESp}AJjAu{7Y6Dy;(SO9;pE?y&>#8|4C#E%( zmZ1u++wNTcyxx_Rh7LCgg}$(HTeC_9+goX75!$TaeH>embTs?-HjOA+U(8}nF_Ufn zhK%YtC>u?BE9N$eKBeCMXB(rHfh%cL-O>S;+;vN5c0javvp8a&EW0K6QVAB2Z2f^7 z{Q&_8xK)ZM0g6(1KDsCe-51!7?RzOQW-!d&eSUYeuGB`yzr25txZ{nTJR#3I)%QhMETkFacZT@kCFkVbxTj`{ zy9tj|99A=}`2yOViRq#4gp>e(DO6AueF6OHVd!k9#|1^flQRe*DioZ**kt^vlibrR zPtbHmXp;seZ_Yr>(B9~woQrlMcn7{D!Z(U4#uxMF+!4AFyp0FB?G1bV(~D9C(8_h< zorygb0wpId+9{(IOvM=#10;}q5|)aRGAHI2%C+Q^bNf^gQob>$J6iEo?S-D*I=Fcq zn3yaaE{K}&0QeuMm%sP%rr|5{_Pon5V}Im}-((yW;HdUF;T!TEIHP$SjNrL#szPyK z&-IX(#l;y&dKi=yOhjyjSJ1Po6j4|;B$f-sA5r}OJr9B_!;%{Gs2<=G@+2K-DHV+$3a6Tn^D~$H z6PLgGKd-nv`a%lbnxJz&9o?&R!iw`AP8np#B7vW|7DYP-BJy?_Q|IpxbRZg zDpI_4NjlC6;UgoSCEF6*&ozK8WsK zM|YdhPFO8h@jd1_9%dGuNuZ5(*jPe-nOTa9jX}WV3;$yfEtF>teHK6X?gNE8;h278 zJ5v;=Aaq?328aUVT&g4BB9uQu^DI~-S$$T8w+B|!rHLuZhLQoHV%4L4-e{5hR0*x! zSm!%5V_tNla#Osw1A=duGPd1!vR_cJ-qck-UfqDj!P$ zKpLH9AOKlNehsS;17a}}AU-Bu-G1^M4gx6ECRStc*Mi*nx6pey#Y1rbpY*^IHVIF` zoxY$iv!Vowfg?^y4Uqh2Nc!h}C2M`4DfM0rTNG4`x&VQCm){A!U;j5(i&kQ%&%hyp zH3W8$pkPuc@;E9=K7<)ULBG6|>yBp(o$WjEW<-UTv4yhq&Tj;&rk!1jN&dgD>dnzA znOz^@20ch-hyf0JPt8)vDh-C+{X2(!n7#RyJA0b2thiO1(Q_j94= zmU&heEhCXGyJW?W?kySLIX5AnvCJ(><4vloIV{o!SYVEv)q^1tNjzuyn9PWHNrVV{ zMOcvFTko5VC}<{018r$Fc-kw2z=B7wV?hXe<@{Cc94I0XcoQY~`?1g4cNAfa=58R? zic1rkT0hwpY8Z>BALQEl_?6{l2Sxu87M;(Ukl-RSG)FaM=ZE=ZX*pwFPqYIMd#dab zWS$x2r_f>e^d_l_oAJZtO4$$`^CByBR*KVyKM$e!n+z#`>8Z*VxcL>QAm#dilH0;z z5rifg_Fh^ZEhYY-Jcs7C=>MoH zU|9tz{C9W$x2z@R|3=o*|0%Kde*%{N2YKoLV(y)yWD6H8-L!4nwr$(CZSS;gJ9l!Y zZQHhOduMi@I$c%WW1QR7_uhW!hgdK1u*UcwB38`!<~RQdivBaK<@hHa`@bF5viwVc z^xw|tzwXMjFmwKoVeNkcq>skVhD{&i69kY5`eX;9ff%u7CxVb*jUpSFDWWkVC-wUL zs!N?w$ua-HN6YQh+|_e_c0nJnsx`Jsq5`|TT;H21=7)rv|1k^N{f}AD>H8ZSyq`r# zG|PYTqbjEwheU*~NCWlq@-0Jgw!?D2#^z0%jZ2xmI^=HDvl=yyu%C~o)`mEu@%zk6p;(d!w&xGkzxz~Z^$5COqmc%k9?`Ta4MiJl(F(uqRhHO@ zd+f_>Odq(w?R}u50b2%lQ{$msAloPoUjm>w2Qn^N0@Oa=f4BV1&6KTg2&N~)BZrnk zrQ8=phBUGW1DCQ!$x!Ho*Ol|zi+i3&hXV*JlQzm^>e87b1EBka@76Q^5TqPn_M(|r z^bb266q3U4k%ADl`g*0Pl@n{Yc3a+1PN@5pvr)EYiA>+I7A07KAZfM95gZ@7@-CDLYjn<%^V~To z7Uz9a5(a<~M2GJ9j8}k)FmIH`%wNg2_~Xsf&|-!$v~Z;8gp!YZRBNwje=k%f;2pEux!y@9tSPzLdcO|)VX zqC^EzV$iTShcZYlv1D(oo=!gDr?zkt-?5gy#XDJN@uP9>{ax&=hsw%00Dv)VyRy_# zr!hRl`*^U0;F^^nxbz2EJ%B$A8#DlyRR*nZSQPoX9=ZeAU-g6N{u-l>bBTKoA4CEU z0oE;F-lM<^d{O7MAKCQF5cjD03>zq24P_V(DofyHnC)T#wj;*u{_bIfkLW zJJpoFYS2E~hJn3*m3#{H!@c+(wgvl-ah%)RdpjbLf7PsOdP|g*ZmX7dtL*~Xefz)J zDE8`V2DD5wR=!=q*&I>Ar9o5KdXHLZExm}#${MbM#rJbR)Mer4%ZJg(7%3hfLjg~W z0beX73Mj@`LXcj<<#F?%{Z=2vFRvc(1q_A9to|+8EIYx7)8%pznfqoYU;fN_x&t;| zQO%^sqJFwr&quK`1*IQWeUa>}Iu1*!g%MT~0(kM1eWraw=IY}THVSoB8SG1z64t*k zeT|7!;!Zu_QsnN$01BatdFQG{{B#~T8y%jLT3HkS_IGCYo+%gsCi2V+vP}kL9*Uso zW*dC`jv0qhtGxiVwI#slw~=1JU(aU@85aYj!Le^98t%5!3tH;_R^s;d0IN0+9B>6` z@qUmJ9vD|sqh3Gevshw4m2E}}fjMP8uQqf6K~+$x5Mc!cNYPRM4ZH}oQUoHL0;bwB z#&$Uuf@-dQM>(G^O}qpBMSGMCm%32|XYa&2|11>FGKvshrpGJgfcIcBK*!8)FoT9q z;OK2b%vO<2fK&fl{P3A>A~fbuwsF~OhF5EkD&R!!R2chf%tV9dlLe~TV(EHul`#z@ z7~JDhK0PBuS5YVSTv?oc^L@?I=d-pi-zo#`2_fM2?AK>hi?3H%@>V5{l?1si4)-XQ zvQ)0go&<}wcLAKY>pb;qF^5O{1TE3YWSQ6G`n4+T)dcYr`B08`_L}Vh%AbSunJVh!DtB7C1h&GY#?Ms zRL@etUm?B2c#cj8Bal~eUtB>KdnAYJl)%y6CyF%Y-b*w9xfE~Q#yWc{{ovYZp=n3%Jh7!E|uEg2#(nXyMZv zsl&uPx-nz&Bnusp=6v-2eHfE=>&{=(A%yFMDa8_U@l8D5g{7a?EuPz1s!u`ohJ1YU z7nl$8d@kEi>!^66!HL(p9tOnPPrY(daGPWF<*O|=+zD2W_1ROi`{9a0CDvQ$AXng; zlw#`2J4;I{#<*Uw`Jn8OV}G*DK!Bl@1I)J>b6K1=|%C}tkL z?>1+d-JZkVDOZORs5-c|DSOg+I96DqwjBdXUjVNll}Cf~mvg@F zz}XUkFlc6pwNWKj^J!XnTA}Vph)|nJBNC9Z-Ra^2;xQAf<5V4SV2%^Ry?(J%kR-sj zHxGv$l@e<^_aCfsVc^+2#zHM}vH zEEwx}Gcn#GqCcE!a5k4BlF2hFL1<6H4XbyDjbQmG1r>FlyWA4X&wFJ=jQt|4CSPVd zb7w#cM~Ru8?L8v8N=<2JX7b@d-NjUTZ=nH`kAE%Js%S7V2T^V6=vLmy65jZ^dG_P@ zNQxz@ER6$!%{}Jh#65v{uBe9S0F_eoUB{;K?0%%omDoOhmYeA+auT2YD~a>J>$HYz zRE4-1tN>=`$Hn2D{fTjzumgyi`OJ6AJci#u`Ru!Qk>Oa~u<|K3=U5k2T@}3=qZ0r8 zZRLvJ{j&)bca$n!n;BP(E=yEzpoJgat`|!x?mQ_*^lZLFpP*Ci=FUO#0JwF*8k7yx zokK|e^@WG{jaYkVPOngOL%G#C>kC7~4>ajDR#|zjUxJ^lmT`OZwqgh8RD4I;O^2BR zu>%`@;WO**v7Zbj8EFfsQHYiGCe=~o?GYkjF^uA4k%;&Z@5Hrb=lRg|VRWN79#Iy?xc~`jk#eHFT2ShTQD5mB~QbDedR+e#vFoiX#2Q zQ9ynsmV6lJAgeDU9Z=cYZ%bdQCvxwF29qI zPz8!omfuhbLn^XNQQea=V3#C1SnE%onr=i$bwFc&x$@Di-*SMD!d*QA3uMm84c#Vvq4PWERao~AXPlF zCMwl&*@UJ*wo3Pg@i7gc-|~}~bpAp~fwhH)2u(wILO3rb=7dGs%PCW(mE0wRq*mft zoAp?KC5QLmmbe7!gebspIKmcm_eASk4Ar_bY3vChabQ-w8ur`iO?=mb$K^;`X*jw7 zp$^uiKdfSLT4i0uJz*dsJC|Dg9YBWTZfGXojK5i>u68%1P+PbKd@Ju6v)r=#?FNz9 zscEMBUHybwcxK`yKg`Lb>)b?jodMBfId{Q%`e$ku7c;RocsJ4dX6D=|p0X7RfZo3H zN)qT~xZF|hUWIa63#!)-yxDz)6`ogUVgg8{UD>X=*29IKHU)d}kAgYo+XgEQ-y!TO zoHF^GI7RW-RqO~KaNolpkmkT6{}v9^KQu5l^=&X{A;xt-Pu@Mf~W#_H+Lx!Eq*o zEQFdths~B_?K+LHC`c+{|4wnr^#@u4dnSY@d)Wo|H6Ynb?WsqWH-aT`PH?4Mq3kXV z6`eY&LZQ*R^t_$pkr;%R3mz}Ri@i8WAp;}pY8 zvJwOL3^Z={6XDJP5v_$4fh@*r59!QJ>IEh|kqLLXBw|*R_iKa3a>~>IopHVEucRUH z-U#KYE?Q!rr5uDS(8FQEuaS^S?F4AOs)nfq*IxrSq6LMzNY%P}&P6bFS-Y7pQK2i` zAIY`idL8V)foAdVcm`*rO0!IPGr118%0nh$b37$&XmUUJ)NLSBDb&LorHFZzNI@%c zbmTX5V$kO(xQ(R)HB0SYyGimiCkE4`G>}QB(2bE-*(l}0&MWJeVe3${O9+6tlFiA( zC|Arbp@53*XZglmO7%?0elgkLJWHDj)lmr$RxgZ7JS{Bl@@6I0bR|QSD-iFMK=lEA z6$fxT=9+jxwo$X1X)Vo4cD_%gvf*0uaWLioJBgmTgz#-JuA zQ24~oH@6qb4>?Z{A}EkFcjz8-gxpDwtEeK5zyW+qcG4SvKNI+iL_6#UEQ;ncdc4fYDdbxa z>l~0-TS5av%?C5x3Kkop;rm0zuDS&?&ss zFP2kkR@-K1WXY=&Mqf^j$3D)cmLNi-6PfoyhHwBPIKV}85HW@7)=z5<2~ih-;9n-% zr}72e4cDYSFX6mlZpvzda&vv7G^^jN4H$28=fZr@(ZG%Y08zciW$@DG-6bVm&IaW3E!snwU84 zle{HCGaK5DD_RRnWTZ^v!-`ngXXdt??K-kz4tb!Pbq9?&ni27U!40JK-%$>IrGe_h zYv*F9*Q$zkiRXaI=Fib3;0h{i2;}Wn!t=d)pSDGRiB4*|GlC;2iaG;dNla#`C)kBX zAL>#f;anqIo;?A_d2rCRj{v7vkI&-3nKYrE!|w+r?^?O6o60ZVIEZEDWTgUY3;5`j zwv7*Gv7H82|84L7xYGY&5?ya*F#c7p^{e{MyS z%Z%y-vbP3GINY7F^*{PJQDUzK>5#_EsnaI@zE8tQcCFo;wZI}|AdO)<9cP%EP1CzA z9s6~ct$w_mpsTX#SE)<0ap$FKKVQ6g=zR5=Ee~te%T%sfnI*?AWSjoXsEyOow0m|} z>a|e2eH~bJ>s+nAC2v=s!2bwRZN~$BUm3nU#-@iC8XhLF`_R0}FgEBT8y?P77{-3T zm=mE9nJ$20?@2VAK~Eg~i1Yhlo%-rYj2B89Fu%=BUa6#B>6Co%k9?3#J86i@Ig%WU z+f3$a7JBj^lgSlOdQ_=met&Z^J0|AO{){Cte-ap(Hh#yq!HYerK@^-PQKGD@vm7K@ zki7Yz$Drs}d3&j5j~CNdwV8Pl$n22@C;M{RVsfQwy#$YInT1}s7P$}CNHZ)+7qhNT z22LsA6?KjJQT_@y8ONN~(HD`rzo6&c(yUTmQv>Hqo(?>)6hsgC22;BPNZzs&e{5Yx?BLscjg0FaMV;iQid;IAB6e>QX%~s z?TlH(AFqGMQYuwGs$Lu_0IQKKKe$fW#4R4Zs}6I2&Ex?jusCE@Bur^wx@57mK~6oY zb7geEjA5Z~0pRP?CnoL9zydM^c?C>^>aIpexK#UDu10vqM{V80l+D zhpTMh0l706`F5 zf%qOfS`tUtEm#MN5`aO{3l=Ui#e~zIwE{@_eI*%@q=!GWA%EcXre(b#xWR~PY3Iw5 zzN#}^;scS*W3{sFw*EN9r4adzL;HqJ zBjN#{s)PXSw-o-TfqLs;CV3$+90wCev2J_(g<^zOKZ8Xf4s)M{?pO?VSpH1G$hC!I zp$>s$aI5jKCIlU)J{U|f+=+&hz3a+!va0cpB7)48asy(SNmBl8rczGzHM_?a{!RIf zz^^8}7tl1C8=A)-y&%O7KFrC3gbsw{D5xb9oK9oE4N#4+S78sC=Ih>&&DCBFj(k}H z`zy78k6P(r&jaDk;5oqRQuSpiVjcm>2=!0>8Q@`Ep|E0x7LaFah4q3m>6VsDH;nJI z#x7Zyl6fLvQouN$ucG@)f&1v!N$=Jl1Z?j}GG`+cG8l)5b!ziKn)o1ocJ8%B?ue90Ex?9RO;1o!Fx@I}v+zp;eof_AvG zmWxEbw;qQ7Q%Hijc!k^0R_Cd0TEl;tov%st!Ak9xx)3)-S?>UacrwcsUxm|(h}Fmv zkT^mBD<$eV*<=aX=y-Pf*vD%fI|*y&pAC&Z-zJ7TJ3Tu4^>=wL3*w3nPvkoaRb%Vp zg6(fF-~$&eWMIJgHDA9y2%96S(o=mfAcshX<#L^y8k(#&^_u_Oh-Ud2>j zyRY9R)w+Jjn~>?FAgN0vJ6$f-f==z>x{OS3R9h`XENIKsT6j%?v$V?-rgO})=HoPJtrG{u! zEGaxu^}HU??$debI(*R(dDn4l97v0?x%Y~rhYF!oAuKN3TAA;cLvbARIiAAZecSn5 z*LdH)xo*YRd^?WrW{44>Y0*V<*eX{7!AFymJ zp-c!gck4Z}Ha0JGgnggS(JfB)r?mI9awvyyJ!NWk9B6^h7}WDqDG_m@``82Wnk>sJ z{q%}K?|ti!+oEY&H0m&+=JZHDO+l<|Fu>@M4H+qw*bi{n4%;12-W|bBW??;dll1eql{b2*b?+8lslsu7|8b<*mIzgLL_XLr&AHjZ@d)Ka}#jy@=$Vm@^R9EcwF zy#&33Yi=#M?dT@?DPxABWEX2%G`5FztqDb;O=mSj3DTpTzr%0`pp0TT#J`4dUyez- z!}Fif&u7Z*Ub|V`j7gR*VTSX$w+Apty6;ZMoO7gUJP3pHiH)&_~!coVtkOc=p2asD1u~6B1stziJS`%H`ra>L|)W1zlC^ zc!2d!GWKWl-a8KgcO1QnL6NPzcE-Gy4uOrq;j=rAsLG-;g~F%{(ZbK;PzP-_!8Yih z9%?}LL!RDNzMHv>7CMPv;Q^p++Ew??9 z%)M$fi{0mAscALK!?`3CZ@~*4x*jBVs64&BNF1jhgYL{ZGypYpKL+PDiaION$?l;2 z;gHh$J(``(qr(l~(u{Bh6?5;h4ww_d#9Oco(gYp>Tx>F*rp^2X{3MfnTISIi{2MwS zN_U4IL)Ux7*{~rPa($_dog>=-`$SC%Qij*e@y2>1>;$*TwEEa(D0t%HCR4aby!ZxR zTAc{|N$$hI7%C*K*%E-)Roa@O#QAHLo>yqRhxpDztZH6WH<%ygL2a(vW=YT;!*uxS zjs8frezO@-$y ziG1@=BaW)Md3-1&P^|D*1Lw=ugAk(6EI&sCJ? z-cIyL`vd&`!SlK7EWK!%u_?vpXcVu|jFX_kGx$1A#Y;H6Ot6mn(2UGd;%h=4DNQ_( z>wRvUBW0+YCCPC~$xU`cflKmmh!oyzp^AS};1^SGhS=D{9y3kD`_CLv!H*jdmB0dTcKvCJuoI{DYy1GN6;_CoXlk4ixwRPef&ec-NbJj9oqXi&;VFv zy;srf)AP7HpO`ps4ItS>q%;)MBnzG)S%-3M%3+{9{_IMMhFXtU3f6?p`VHLyPJ+l} zZ(YMj-snX{p>nrBL>Ckz+MXd~Jgv+CM$Z_p|CwI-w5nxw3Jv<=Ium)3cd3U7U6 z*sZH0+DBaCHjm#*#7k!~KrH%olAEX+2|~+}>M6J+TKhYngyZ`@=VKBW6~TMg`eChK9_R?5;7a;{XpC_IyAq;-fJaB+(8I3rgq2r7WbYiKZ6<%*) z7Myi&lG(wb3`$C*TU2Mw(m7lzE;6v=hqP8SmI$~v?~9|59aXk!W7<&ZHLa6~Wc4>|};5`4&46yU2T%8{P5D`02+KH-Y4R6#g zAByxWvZv;91Jdv%=qVz5qUPUr`_U|>!2V3j$`sT?Xy0E5T;!-)5BzP%H3U6oDF}b0 zIOke?LyfMdPg%xu#qKP>{FQrsaMTAv;co55PLK1h}m&c)AqLxRPTyomJQq^Fs zEZK*H5ECcXT@ktybn2WkdYwz07s-q1NO%MHMnt)1ehw&}QXYr8d^x8Hc-gwt* zw%kj5%gR9;mHC^K#YA0s4_Vq)U$mOlQig{7OLkca0LPAIDP`_l4gcVZXUcG&RC~K z1AUJ#f>&PW{YM)7_s?M7U6`-mzsi~ZUzq@zS^ow~iIofuytp|0w`W zlL2r3SNH#y0zj63#z4YO_6|b!9@-3aKYZEGi;a=#=gQ9cbJeAnH8rs`{C|8`!O+Rn z&gG}&%KVQTDEoALzlkI=x zx3bkW<2FVS{Z?vDTUdfr<@0#hLIGzk!Q2ptfwi;Huli?CP4W>zBkGwoFMr9avf3x$ z{DIXFF3)oHds1E6>QK(LLz~F%@^pTon?ax0@)|mJZ@3)w>*(m2`I-c^;Rnsk z3koh39BdH&cYN!U?PElwkSC(>wMHwf?~)&xyh^m7VJVpT6R^p$9ULC&Myep^PW_k% z>UU~GT%XF_5M{bm_onNQ{*P!OMZ+6OFJjT6RHcGgoZS?oKt{c+!bD8(2j8N6U<|VO z!M6fx2N8boEf~lUio*?oknFv&gAM`euh;S-ZX_}lS=V^7qlMrzO63xt3L`?=oY|m- zOi?lvy1`AA8ukVrS1}R)f^sA+vZmC$f8bl4|Bi2kT8L)mEGJFrlk@)qS>1&7f8?8Z zxNHkjJ_ru|Wq<}kVTd3`m{c4w-(#-%h4r)RcjaqqJS)JJvTR{;WPKE@1UY=L4hjE} z>wz|_zW!Z}cpxQ(2ro7diEfBNeDd&xAOkxS3m`wH4@6721y*4Q1rby%-93TWaH)QU zvY<4~-&-1Z-un>;adnBE98XU=APR;M_nF22Dvb|h`Wk?j<{-@gfUr}f@NSSd>yLs@ z?F(g%bUu_XWYB-rQ_!CF@J2UFr z0N!5&LRVKR0b@yD#jX|zVr=7dVyFG!TYx|Kmg$=5Zu${+jiFX~84CsBL4_D9+o6VSD~EuRx<-1OUtv6n%EX#T4*gY2uHtUWg&@di-}-e8!aCBz{3HA=VA zT2D(#TE!#M9b#H+FESUBWj;L;;1OY?LP$Lkj_#30x9zO%i1qY0s*Em-K}e(mz-3K2 zcmq(XZP;+&eQIm7bCYBH#g6e&t{utTGa`KZkE0QLkO5)xJQY*Tyu#wA3G9`Xs}K{S zL=|Zf%6BtsX@TdzB_2G?Z9(xUa5M!}@(NXBlV9~?zySYr3KcSkfC!Zrf6qC(Z4=;> z_defoc*iLV&rQAk6-JM9OV=$T4pU+#flX2t55NPo6r(S3i#845R+4^Md+$!8MARus zB1!fQBuToL>*wR*LL`Hfra|XB>`aO?YYY9fPI5FweoCC5iz^@C#IsSDQnrfiw~5|d zD5)x8Rp>yvojtMvwC2%s14nmtsMfaEJZ-0sRB++BI3;>KbTg#z5-_tbw4yaGkudzT#%9nXu~AtSL-yk6eRP*G47h&MLQL=>ahsguv@B zBuYaOi1X^qVa#uwCM;)u)_o0%;+C#uBYMo44$p9}elB!r$rtY({MXX4mwy+~$4^s% zwCdC#Chx?VvK+_pqg@&u99u@Los6Zb-N1i$@Xwj+^u_#r$a)!j+#Z@gl@7p)I>32d z5xlY%nlpTG_4pd%JiY^k+BSaZ;#y0>l%< zlzQLZ9>dMLwuK%U2WR*A!&8%mMgcaVSDMJG0A=0{LjteLmce;qDsQEJ9`gHL>3tcI z4N34sJx5zM6qiwM+%!v#*Bq?SO#Dkm1*b>~cP15;gDh)~0rf1$hubl#uDy7WKM)pW zORgs%$vF#rEf?>;#0T^|6hjuZ1F#4@grS61kPII51(j3*lbA5>ny2mW`1hu*x*9bv zs48u0>Ut01y{6UVy^TmkR)qbLcNth%yl>4+hq_8Fct=e!nz}Gi#mdn1J&AQLf=L%td+#Qa|nA1ar8c)7cI`_YD&&3#`a~7+PVC5ghJ&qGfWQJeqkdMkZr3zNaotjwzgm&ei09#H|mCNy4vbPN)$V zg zEKZ=?cBbaHI5s8w4p0B0S&@^`W!_5e@G|8z%5}dDz1OznMBcBA0>VvpZX8!bl8|b) zoX|phkybs#sLVBhit0jgo2(FBIXGQRWr5Pe(u2%&N!Pv7s!yP;#wPAlHF6!TY61=D zH;XXKuLnL~^}O@LrW^*q2p-QUSGC0$Dy1O~i^V8|`L(u>Ji73S3HOCWaR$8N5En_# zU4J4V`pLwyI=#n>Vd$J5#z?DNB_X3ZO~(=`oqK%-o_f%j?a*9$=e~Es=auqRydf<0 z!v1Khse2#9-jQgs{<&4bg*RjU2(kApZrwpVf_|HE0$#K=oWZdz{-$19c zbLuj#W87(CG5GW<+8#1DsG^*;*p4(oYI*8XB8Joxy#9dTQII?7^V`UP=2`Riso<~# ztjn|4K&_(%x0u&n7p86+wDA-c0ltE81``J%2^is^cmjPu^gov5-}ANZO>T>5XO|2} z8@S9Es8_^HxZx0&CXO2KDk_`|K8^v>aF|jjCO@WC86gVB-QCo$H|>n z0k7LL$ZG+J&X~IAAv`eNh}>Iuh1NIOUg$%|r!xYUJsiZ@btSFlhi3IX6rQ#=KmItDme7fj>ekDuCU17AcvAy77a(~E{Zq>;SSd;oKBGce&7g>IUM1{58NXxTbvrbxV`gI%{uP zzV0a)pDX&~w?0k83(8BIkGWm3>QB~96d{U(h<7O;)M%r~NYCSt7&VG8?XekmUhKA} zyA%}>2Wb?mTrkd?E)hmW>yfFf8NsLHtu0~)2Af6_Fs}$AR_sZlPtb;mT!zww&$olM zVewR^5PxlHvfU0vrIcF0fr@Y?E=pt7;=$KJXk)Q+&RDkNm&qecyqVTb+P3{5d6kIw z`@&K|a@-jgn?upe>4ONLPfrasTPc++B%B}-6v}%^8-f*mlgFFxuoa!Pt19!7dEvaYM-!D_d0(NeQK3#Gxq z?mb#l?U~`xRi8F9U6nt_zxG#@Xg&6>%5qN|L}K4znozyD$0T@j4n13#IgNgi%VLCT zx7{{JN}IT}ixdS)j@TxeIGYxtoR`ETLPD*oTq(LuzgaH(z!E;Z3AELa&W|o3*(_y; z?ouRTE?KP0AN`eK zEUC$e{NjyjsBM_v?x%F{u?ZQZq{E5~2I&+0KECz(%z0-{l%y}6L z=kGh24R4g)T6n z%9CI4YO8P!L4N&4JTR3&*SjtIv8dWB(WYY$-08_ZF)IgjC34)YT-k>}{1&U%kCE&K zl`FB52tarR_}MfdOXh3|HekrvL1_UOpYU+qSxLRMf_M=C$0=_dGT~HD+hE3NQNcg7 z_MWLE?JIlc*OI|R^TbxN!>~DDV(cwlR_AfA`%8r85%jQbUFz>#{VaBsxFa<#b#CI_ zSYg#V++=3J#mxy3(WD{P(jyZa+$gAolQ>02X_gWei}$jPY*7H9kVIPfi~wKX1D=zeTxC zZi!<{368N9VztZ3Lr0WDmyU6`4g0|}P4S%T!s&S&Ak{X<$_&je;@O%iwuji>9KibD z6{oCfGer_zdUTw>xhE<}d@2%xn5UZz>RXMCDXXB09EFqf2qD={3sG(EKC;)s88SHf zOs+njkxXuB;^Gtwkicij0Wz4lT|k<5HZ>XQxV@$4;0)BoQpg@Us-)z7$BtWL#K|h3 zY$%+p`Cu(CpCL@vGrN!oF^8o<$mvNvWUHEt$wZSJIyLP6;y!Go!qk~Fmp>on za{gAZJbFYbS_jLnAvtg@=e$7rxv^Mzcu5chbCgETt2{b^ z@FI7PKKk_SIwarJJDIvk(ori4PU@$HwN0wLAcHZdX&0s;H4T(tUf=9R73?@h9gE8p z6mzSU`RSDwr*ZY~tA48MXW<>oL_b;n71v*CHMD9k3`)Y;gUVxa+@*CAs^lR9BLv`g zeSbiJeqR;-e-*m@E8*QgdyfA(!ol)?PH_;VS}b7Pl}%?jIj3b~wa&!#PUe6j{!@4NluB|rWQ+H+3qo#;&1kEi43=C_=f z2_=jns3K8Ijo&e&)M_Rn@$FQGb0Y&T->%9HS@`E;o!j+Uh=1F&c<5G={?=1UGsK*8;)Xwc6RG&t$jXl!L}zNy+Uk9cuNWVYjj`YfQ_eTe z)_*347nmywr+`+nmT%H`SOJ!ljbIGafXq1m;k!$F7!f(j-kaR26ruKeDLCmsAXC?M zM-VCO{*6-n!1d^zs33C-2P1H0N)dZplG23`Q-@{k8 z-w{6F%$8i(Fzd1Bdtq8K5gO=;uYmeiR%KW2vCsVLoWX@<>0W%m!-BwC63xMv$Q_i3 zn)~z!GqaWy5Avt0nJU?hVjxYEPHG}jD!Cd=kGyllo z$vk|vEV91K^EmmC57>8+It>&oZncf+GBip=QvzdOg zFSylWYNI$&^y+Y+#`^HoqFXjkRf%mvjET;PKO@5j*=Lv|)l6Q839Qf2MvYQISxr)l;5(UK$HMHFf|6>vfdM`lF*tjF5O*Ve$rA@#YLZb$oBj$ z&?@)9^_^T>x83ccS78c)?GiCH=v*3RZqRNJ19!nP!QZJNEcfH4;f-m;Gy8(y*$n_9 zoZS;jeCh>9d*3QsHssp}jHF<=J!dBT z2Qm#wFUx>@c&$c0rHD@lG7ScaUg1ge=t&ssC=d!tfKzt|@&&GO;rB34Cav${-8be( zBN=XUApR2$Tj)5H+wXWw+FY(D!pwPxfR8f{R;6Iu@-c zsb9*vjY_6J*7A!XN;ZT^^E5tltz~4E=J+w_(6*(jx|cr?=DojC8}o%pz{i0YJ*!z_ zheR^nH;iPUA3Y(?XtG76VAs(?c1TBy8_1oSg_5SJVY-nv9ik{>YorOT2BH}G`8Zrn zxRgi|m>Gc=C-`MeJ+$w}4{5|jQxJ?+hy$$yjgR8+e~{cWMJ5*TLMb=I*I|rQGu1@nTIt1 zco%vK{Y)>uZG`f4TlrWukA^e_I^Web&=c$QYsda)6Jpk|PJlBij&U^JHyfSX}Y zceaf!3&m7TgJtb1Z;Cw@YEIztY}NSJ@yN4 zZkm35#(sP6py-4Jz6}_LE3S=a5KQMf()pRG_qf(bAEp6+nGP|M`SL9lT2y?R&Kcu3 zc2^^61Sam3HjaMj%(7sk4Q+0=;XF(iK4h&1{fe3*9=Zk;w(x*5_Ugp94W2BraIOgL ztgEid09m^vF+qtIZ`Y<=*+Ua#V^AVkw7XpzcAbt{AiI84%pTMRe27?vb&5uyUnhVM zK@yW{0h$tJ5{jvbwZ|_6RR9G*1x$xjJ_by6{~0;nb3we;8$tg0MHLjtJN(9CWsFC3 zS>(Nb`?sAy(yh4QuV^%+$>3TZ?(4-P+G?=(rXlh)b6EedMmwl&qK`HW zg`&`DO5~AVyLF@sP@Dck1%jzvM-^whkRZAtgEOCC8UGx#{H-5+i* zyB-qKW=)mqGG*43wR0Rr6I+yFoO$H^kMf?KZV}wot@Iu5@5fW~(YI=m9D~rfzx8(_ zhwVpU?*-e-))1PPnWBq*IToOW2PF))HN0qbrmyk(?(cX0VN^6W1%2@rdKu$byWr zt%)a~5o+il5wtL)ooeqm+VluCs&zz;{R6Eg%*cNe<;W zHprnr1wSsKa!q#+w{ucdtW}(9?KuaQsG1_i5A=v?^9rMxGCGUSl0Y(0(CAh{PneJ{ z!R`XdStMyKiM$3lYrShn-OILlUa)h^$bjCp@rw(_NPyL0$|WHd>*hYY!8-cOX+kjf zLMlDdQyORY@Z(dO;4vsTZG-4N-mJ!x=l4UvUo6Ee>QhNs*Ma~sT`*fGc`9-=Jr zc*)cYGxzxRS#K+B3mY4Q)_->##7_-NO~$ImL`4_Bg~khg&gf9T&3o3}u-Ow&Km0)P zDSNLC^*x{ieKCxIFh)s+O~5_~fT6V#Z4FVC5)K`YZzwl$bWWXfR=3|S<21n_7mK#L{ppaxLUaN~K8!_p230;i zE-t1+oF$W#UiZd@O1QBN@UTd7nUh#2nL27XTtpzyi%rT58i*d1`e}DBEPG3ZnV~7j za3R*T;xa)Y29jmgEPoMfbSeNff)@xgYC09R6x_?z3v}2xI=8#qL@z5=kR)w#5N+yX zavZ`@+VM0XP#O!k)=*>RrlWiW@e>ozV*(kS;?5=89!rz*gG#g zRRx`Js+UrU=gVH8uDpOjc!FA=Gkyc{^z=nNRt3P)9z=!dFo9IiRBIn%C|JfS?D!?$ z6K8plNC8kKymP4(o=VP@g38b92wwUST^`++d}~Ui6*bzUkxCYV{B}@4VLu1o-OZ6MO#75k` z0mFn^A)HJ_orx`{8JK)=Vhj;|=DXiU)x}g0tT(8j3Cawm+9QN=T;|_PtWRw8rSu#Z z6nKlT%O{AmI&jljoL2}WIiI&qOPc2tGa$aL^FI?MMYZQMT*k=>)W)e6JJtz&yGY4i z8)jQN4>12#fQvSRN2vj3x2@Sx#~7%ezMP6`27Uv+W*#UTGIC;>ad#(;l^H9K3Wu42 zHhCv1-@QN(!i;XOVZ> zDCQ0dVcQ%yKqBvOuDe8g}A{cq`#>;U&mGQ zDd01IXAIA&2S7>3J?d%3d7-y`HHZFqMY6}xG^#1C!h*7%4Ut-qFr?&6`W@@MjA(WX z(k{O9qE1{{vS}=D0yRn9!h{1BV|PLH~2MrH=y89awMh)5(7vaI&}DNI@M z5(_Q;23OXC6l)(uwM^*-Q>OGtTr7DBEDC3aFK|KXo7^-mzC*(><40r=TPSIdJ>24t z5=cJ=(6<^^VEg5h*0e@qgm6*sLK<~TdUKj2iiZX32)xp9r*(_!2+)w4^RbE1dp@0J zFhCO{Xqsr*IC6lGBzO;fW^)aN>`;g>pC%aM=YpP%Z2sYbP2xMNJl>J=Q8c(F7)hu1 z6P>&tZ@6(Y=2bQHT>!vt9AP z`eq4fax2+}-|2a?JlY$Ye3OBs2SBMR8Jnm?Y8$5?a)YeE^~VK4^xa^Z{3N617dMZr z(VQ<+m8;ZAFSXiPr}4`U zBx8*ldK01kA>ggj7mbOWcMc8oV4D$c;NengW#STA|5J8(~&qf zWZHaHx!DKyOgm^Z?6-*r;iv}eOnYI!a=j4=wXiA{04XFL0A7GyCwBUP+sommzb{iE zoxj`&e?Tq&jvUxM2Cb3j-;eYIzg!l{V&$sO#UDG}RP-9p4_TS3-JLDHBdnQ@=4Og+ zM`!wZBr31_sO0u}3A7&;J4&J`%E0D_X!VJV+kU6b~_F3nPNSL`}T39X#*z<0l~(Di^(In{m!(OUSX zAXS;sf(fOk`b88px6w$U*0zB+fk*awm?Z?!`uU|@fB=FN4vNCAt=K{l%-F`@%aY0c zqgwMmX!aLM=xyV4CSf!f?lq~N+x2eqx4|~;ivm4|^Zx3q<2P|cP%07g0{0l0t+>}I zY6&f(w*=+EYnwO^fop+q8BKfzsYNm^p+m1Vi?(3pW=h=J7i|69+oM%Ary|$wi4NOT z?_Mz-O+dSzP>=}-$ifUhRqWVsKgzT3B$)7GNDwfmW0r3$<{dg}=b|9uG0-@JiAiIp z`p6DaLz3W;-)Li*(DKSq><54NY*+QPT^!0xcXlBDo*FvD2~k17EVZBW(3S7@`1QJYM>4kp?Y+no{Jk<<=?P7pl-_vx z2w5r1v(heBaWFHF(ji(C=+Fjt+UT^RBsA&G(j(Cd_(|ApR7pL1vHO``z@-pc{Dw3p z%LXyAS3G`3CMHo_VC*Gy`#gj=JjMgy((WfuEQy1n?Raq^gm#{J> zZ(4vP=?><7A?RkG8S}N%sl6%D1&_g~Uf;|$`*nAKqq(}TFGeWviOz%ZD9PEQw+#H>j;H0kyuIlD2 z@t2c3n<-Y-?Osoa7qgCI;Feq|mjp>1&#{rDcB*lT7^I4pOLfz3>;WxyOPK`{Y-C(> zK5b}Z4x-C%V;UwsiGaOX*6dn-TKe%sKWKtuc3Qcfdf0@J3MX7P@%8|BL%LHUSA5?S z0oP`a{Go-{=xE2_szDj?E1KeG5pIH`VP$uYsgHodrISpV`rQNW&u1$8ORNE62t6nS zibNDiq+){_#e3m-gr9|yF+8;3tVIVwZPTO*F!zKtc2D331qSuH*k%=iL1PTG69-T3 zJJkjkn}kfl2C?0q&lM^W-yT~j$+2Hxj|T7!AzVWsA$!?3zQ8Hfj z1gEW`2AWc*q#}>X>-9OT`!%Oc7mNk-H$0u?|55(%YtUH+{~tVA*1$s1$@)J8O|vb= zW@hC-C0bUnKP1{AQn@u0!3+#Rb0TtBm9K8T9spSxRMp>I#6Oh~1#E0=ogDF*IQ|0f z{{i(`{x60R|6hap|1Q)1%ZklEQ~m$VvNj9*--Z$Yyhr~t)Td|pPhrGHRZH9TR-}(B z-M*NGe3KW1M>QvCVsmsh^z$x(y({z*HS*^8muf1^=J~CMy`Bu)8h_p)*oZ&RxYeG%NO&jgCf89Sid9bqeL_zuF+Qr=_?zl zTNx5Kr}s9lAHQvmbDEm|9%~Tk);Cprb2DVSMqBx1${*pI9d zV^C#V`_~=am;BL2wEClsDD3$!ZAABsx^*@Mee3&n5L++UuQnp_PIBme zw&ml0v=OPL3jb^)LVvXp;gTHkBm-eiVPPwms9ED`5r>B8zS@XZQK5iBhF@*O3~gR1 zF#uY%&>bjbv~)RQNH?k_tArRlA1v=w0IMl5eh>L8m&0Q`nJ84O&mFa$+(bX@AgOs) z$saW1n-qyMC2N~a5dy%wU*821d}x1=BudVHdrPsa<=dHtdX?-r#$u>0L>bK`tEy#a zXK1FqO6gmU#|0+CHLUm4|VUR$PKjuk)Mr-K}w zE)_OmyyAIcVFZ+@*gsKN7^&Q>us?Q?eF3DZevPrRjV&Jc)x4IY+N^Fc#;)4qDN+_u zSe9e(4Gl4NdfWieW;!+lFy0r@tqkF-1=-thuLon~G~6|vST(V~LlrA|@!<*moydoU zU^?p=5}BN+%5)3Vm-2~2j$s;TaQU1hF?263xs|IA2ku7m;Klye`r<3t6{xMZ_+R<~nftFxWDBesJd7xGICQkAAN`Kz-V zaggay3TOaz3(8~SGnKWFiQP8Z0m_=CI!lu><)f!tl%6+Mgi?o7wiK^{V1qS)oX42m z8!Do_>wRrmSXEW~P1fzS8e9Vl(E_jUIbJOAF&7Z<%{F>J!#G-mbT>&5Abzr)dG6Y$ zHG%V-BJDnxopOkFB1A!Hid}J%op~3pGuagMSl(uEx6Wr_jg^3I1Z-AWQ6XHM8eb^9 z+p}@>YKP!X4C1fLTAR4}7{wL}-yHT{dwaAsQu2XwQa6a@8gM+uH0Wh6hJv31zCJ7OV>MHui) z(>0xF&tj&fnFfvZQc4I`bwn2Att=vH$j;EH?lS;S2JTj%H77_UNgmw9e_QDgK0ph5 zGzWr%`gP~KN&$Pl(3p~#z;p(1-)+Euo;!4&cp`I#kelappAR1`?LzXcslGBO^uH&7 zPD4kCrq2WUc(U|5Y6yq1Deu%*9lXrU%IPh79TNDXZaVs6DMI?Is1~OKo~SvY4{B)H+PaAA-#4LnO|HNGLZhm z2j40G^P3S5ac)UxJnWkWu!zVfo!Ht^`qcf!ds6$+ zU!p(b%HcN<@qSiXj(Nz173#l9vRjOX<=yymS#zW4tQwds3S)VwCBTWT4U_O9p1dUa zEun=SzWu^t)*bZTCFoX>K?At0gES@$muRBX4r2m;3mGbLRy}b_+&ctgVGl1vVGC!k zU>4QJi&Pb>IrQEv`FJY^W>8K)N5;5eDHUKw;`9{II%_Y`Sa#&~o%6^54es zfB^4yhrFtj;Yo>r(GNTKM#Vz%msUzZ7DTA;oyI>QY42=cYSB%z9O6kcP@feY(Jh;#j25j39BKd1?ACjhb0z5{KcT$==Fe1~)P|5-VYEQ0?-}8@xAR#Y(J8cZ!TkB2 z{xB!jdYPR*D_A7tA=)fdc>uW}jK`AlD$4DtKvG)Jv!cA{vrxE>a(0`G)RNcp3@%KpH2D3!TlDHJ)NS)A>LYR}SE zeofk_RbjXQw>zH53TKh@M47<+k2F#g%-v(KLF#NqT3tfp#a$5~7-fdA%cWm}1>v1kx z53nN^0jvQMJ?u$HUm)cNY`+QE&AiPEB-H}l2;<#k+#30MoN_zhxZeT05}h?ly8#EF zM|;ruW^XqW_4f5F{L4A==c~vTsHfHWtnPOn9`Z+{E@agsko-$3MC&B+Zq?H!MDP76 zoW*cfyepyKr0mBed+~A!9HUzcAo2PyH=5DyLV2-31{bQUT?G?})FdlEDO@I+`po9gV>6l6<_=J!2uOxXlZMFeB`e(* zI{ncx(sXG~wFT&2Q76sxEAfJbkau@QAvF8$1<#;{6IW@rYOmT}@44mdyq;7d^d*B5Wm3Ds(K&rn`Oxc1 z-*^z52^{LC)WwH2;Hmb&1**D@Ldtu_T zoF7Ta((s`cd;b$!HjIgm+bu6sAH$4#epl;>+Hw+tAnth9^?~qqM#ZM=n0QG%ZRajf zrq*?@e5C*#@R*_{bHg(pBmv7+HL^MYk0cR<8E`kf#IP(&JsMl)?1CADDXs>?ksb6U zM4CQo`Db(LdX`1xU@>FD^Z-!At!W~Q`>ZRDcY!i~E2dURWU{TA{va;#H+_)k>k^Gh z@Pw<@Zv8kEjx6aRd11P^RJ|bPaOwPyV5wK=yECFqkPi#_LbU;S87Y80jJRsZz zCb{7kPNizk^oC~$toz6q8M~udmr2NE?%WQ%R7W^Wr$Xk5%R#S~)cKJ%uF>FT!3{l0 zQsOM>KhQu!pI9>CKP;1Z+^gcY}z|lM<1Y$dz0y-Y15(w zQdA>5*Y{Dc9R*jZ)u~#04M@30Zc4AbI9aRU6)|2=IxC*_^ja=@qM#BfbUWz zNpcMenQ2>3;6}SVgx%+r zFmlgUU?GO=%D(_7(lC@&ft@OvPF8=LJo%ChnLra^Fs;bNl6N2qzIR{0Oj`xIU3N3azaoe{?L8)KjN&Dme?qifvb3eg12tgp=M_ z?Uv#^1NpAZ%-#Al;?%C0Np7TvajuzVH5j&qE9E};9MJo{uA&Vti>y6mmgQ~`Axt|I zBhYHlHSu=0a)mkgfz!al$0R1S1ihLE>|+SxGfQjwbX4%}p6-Guo1K3U*DY%UM^%Ku zHvPCX{S?G>mFm=)a)ZY%2SdE$Wrf>TDYPdG5fRiL1-6^W?xfMw1DpBxhCa7;sD2u&WnG%yS3<&0rQun(4GvGT?R=#b;tT%<&^;R^cQQ9v1cJJ^ou|D4w)OD$QwU zut#>^T@UEfMt?S4e>VD%zwLmoeL)UyzJ>_$33)^O0k)*vNs*vcdQuruXMKvhr*2+@ zRddC!s_m@ceG?%=!{j0)rQEhC`7i@B@E;@Qs=8w$6W<*Xubm~?=u(cVyA!ox1>fIF zJDO@F*3n9)x*B@-H|Olp*_vvO(R4hAiG62s=KX>;9$kP-sMjaT;wKGbB~8di!Bvor zgF)OcdZZ{jStgh+KV7ObvTPJ)2@lYr)QKa=mkF&YU1*Qnt9I;FG?IFe?+U4zG#B{f z;_@Sp?!tWtV@Aipth`4PjUmx9aX>?Gu#__?VSIwb@5G&m&CS7BGpLIZle7=q){XLHpH)M7XgD@G%jLRljli`$+#M)xq=CjZtfx#0db4TW zj9joDAi9nXHxU@}&vqUw*H8q1=P6ac62~#fzFz@J9U3Z9H#eg#q*ZF6+loC!td*uS z@Nk->V(Iou*B;kT2g2l>Bx1872AoksT+h(EcYk7sDid*3o48FYDO69Eg`f8XepaLU zR=It;VvQTwq-}+gH1mUvA$v(yJvqSU`svK7wVtp&FY(P4*&XM5!(ab>tvH@`h&n<7xQ6Qv5{Rij;lQC8r4b!^st;ujAIBZG( z)h#4xTqp%V5D>$!C}e!xkrb-QTItfc!$!LOrbgNQAWGZquW^FYpR^xP`(c|X&S_s- z&~a3Lz7z)esTCIgUE~s!x&UH5cKqJJ5$~g(;DYpAAdF8@6&km_FW5(deQaXgyw22M zQa5CU6G3GwblWLRnmNqElo#!q+m zhJ!|4+#Gk2&xFO(4^8*+$V1i}ew@ zcBD~6QL+hQ#*AM>^Q#v%ePJ2(Sj=83O2~agN5%52>HKqPG*9BYhCc@JuV00@gPx&b zhT>cZTx|esBVdMs4_`m5Wrc%_M&3m}P(`Jy$R4(E93A5}wpdvpc;TGN@bqh`V{&?Z z$dFzj4gfhC>IPa!S2j=LT_{F^C}&a96rR&a%}xz1RV z5m_w#@PcDY-yC{$+B@LswT4lLEgZF3iG(9g=CzG|>jXjwj}p%%I6P>!2}-#^V+u4k zRVw?&1m#BtRrBeV-Kq}WC&_s2S8Zga39j4bvHNQb(o>8qDd5OyrVs`gEg3x;N~-f_ zVrTF8`ITY%l}qMV(vCXcGIYRb(-fWs3mi^I9wphJN#`#qGvWm+bE2-Nqu;(*v2U$( zBdzx4+<4^FcH|V{TKK={4=fG8LDqpTNtn1^1sX>;b8egT(wU(%#PxMM@R}V7 zCoL~}8wb*8P!Tdvo+l7!_leI9yL+m|rh!E}Gtoz_N5cq+4>wy)P!FXBzIpJMgL8K+ z=vh87e07024+Teq15#<Rp|BXz7lG0b3ftmFWw_M??dcc6s%<$KUx{?y(*WsM)(uRJK)Fk_`fF>1yNfVkI6ipx5zlNNywwGd)@+Xo_$w#T`W^nUbc0e zw2+^|x}BF@w#_Kcq^W32)sa1`dii+W?0D@+J#sfo9EpT;Bto+8Z$wa!4~nC8SbH%3 zvHrfIviB;aJXMj^ntfAa@7}VOI9a|Gn)Acw`BPM@D?>|r+bitng@CXi_XMlza)JbL zVk&iYK7F_XLs}T3sHjx7Fd`a-b>u~Es4$wTfx{1$q#w{o>Y#NSpc=d&L0X_`m27@< z3Z?Y)3Sm3Xqq<)?xCnenpLtGL(%-F;{p`UrqwkVXQo?yMkr^Ww!bq?#!f`gX#RS!^ zW|VxOL?q+d^m7i)I46tx<$HOxS81nl^ig8MlCXyl#;J0cObF!~X_!wLGS^DcmW+#v zdN8ZhWEn+_fn6&;Wu4GUl%Og@w!l>fFZcYeXz5Cii*fKtj|5)FAElMw?&-tbgTU?5 zB}R{KGI&155g{OSBqZOYFb)Y>?Ng795~U)vots|n7wK{!IG0WJUaM`=UwwR)5(3jd zxs^~q%kpCudKR4TaTLgqt#h10g@kkz#{xrS>uu%a{aWyPb~)S;{c;nQMS|v*#+(=> z2Ds7G5sz5GZ2aTm9+P{~N5A<+kE8_y=?lLQq=-7K0>tfQ$Ry6lC-&sk%D!Js!b))D0tyK8V zR3s9fdW`GVl3vni69fR`LgMMyj&)0L^KJ@BHDwP_>HjR z5)m}C*ZlH_zy^Cpw!Enrt$T$o_q_+1Ll75T6(cu>f@#j$r=g$4d44NbIL{`E}soB+uHs&E%$WtG9oDX03KHbjkm4ME{PqeN8Q+x!)BKJ&OU3fosA;bZMm){6U z;4o8*IOX@e*F$L1(195J#jXyQwcv42ASt#BtHR*gkjk~ij~6#fN1!R6Xm*`0>9&?8 z_I~R&_Ebi>E5^CPpOBkSJ!m;J$B~Xc`Ac7Ogwx1(z6!GJFf_rAEA7=Si8>JUp4c2H z1Y>vS<%phk)OE8K*7Hd*NzRM)Hvym&D!N0xHBN@=IxO= zWR%l>VPpvv;{)CF^ikUEuJNP+uL$inY}tEhoWu@NJnG13Ft+q5wsJ;&{jn~uMQ%-W z1rziAjBedwzDZ7AwNK0%TqKO02!%x0T0e6=rfbJB!pXH|#C7@OP}uaOZ3?Hm)8dh{ z;l``S`M!eUA?M48jhmc|Fs<>DvF-3ucsf*e0sB=Sg1X4ss0~qzysL&6tD8XRs&D?W z2JF`ik&DK5X{)$$#NP|?qQ{xkUc$($8)f+emaCtK5j0)jL!94@fOm7mF7ja{k+FbN zDoG;qiLCf;|HDOllZZ(!Hf0iuXl!0vge^(8xszLyCKRDAS1GmREVMAB>A_TnL`kCH zeQ@~63ceZV2*uDmqvX1cF}QfTb2pD?2_{<^hRWCR(`=IDapoHjNNNhcujCcE4<6tq ztD}J71xvpbh^i$kjEqSTarD5#;X9pCEX*cEdU1G9{`rW*wcm!vjtPvXPm#4KFHB`9 zIrq~p{VWo4LZH|fLA7{^R>5JiVwv(ZvURvp*<@|$1~}9`y>fQDm?^&yY}i;nS+G8#fRA*Ae!m43EpNm!{r^Fx?%=W~!}behLD>U6`_BS)WU0VUn51D|GP< zV2{IYQI9oQ>RK}Ba%eBY439KprX{m_2QhGwh0VqL`RPY0iUTnSfx=o3OhbWFqWy$$ z3nZp$8{$kJ@DquGgm&ad3t_dn6lV6|)kFSPTOw8EzMwFW_in?`h0oVT%r~Yvmy^5F zQna(hqGFVR6-OX9V^7f96qOJW6zNG$`%Chvh~`7fDL%Q8z6M$nf)_KngizsmFD|Ru zVFy74gxl*HVbKmbAlZ)AX7n;nBxJC4!hNGh3o+=8v}5&x(B zZ^v5J+7pEp{v-UR2yLoHW6?&yPMy`Ou=ETq9k`>V2u4q9`eBhZLSLbLB%FL4?S{J< z10}P$MF&e4f1t}|pWO%>gl%KTWazIwq$Oa#0;7{H&;Ox54IULi7i6Uxlx%3(5tj*0Go!uM(pXZ^W@6BX(u0 zzcYzF-F~1v19^KBU2+L7kz+{2um-!e1;4bOe;*q#=Ilg&8n7elP!x2eP-Ww1@*shV z*=AD8D~<}|ra=$|#%Ex9vV9o2K-;bPR1xJM4l`<3#$(ZURBds;)gY7?+o_78HfwB% z?KdhrBU2+dNPm>oG`x7`EWGqIDTAEM2x^LC6Dx(J!{X7JYo87P2!D98wi(86(0bpt z2b2;AB!Xdc4kL;yKGo-q#RVaPWHV^*k&Dk?Uqb!27?QXda)efv@#9I&+Xmr|;eM>| zw?v`OuPE_A@R|$vOOySIe>P99&tAS)f1bVrEhuOxx=~LuQOW;=j9R|M2M9oI@3G`# zvPl~KV9Iq9?3xjuD?d-Yvjsig1lOSi%#@_caht&~O-v9?mOjb*rZA3PzJF+gd?CK+ zoOKbPLzs5qKc1Zi%9_k$r11;zb5N7})(Gps$b1@oBXSDLBx#;v0q~k3fi0(T2_P4P z9ddZAUzkQ8)6t)-%&(qQxT_89Zc*Z{*ZZN4j8cPnNcBNp&OO?}xpEA%JTiciX!v)Q zso?A^bY1P&e>{qjhd)aXcjV|Oki|)jf+)rMV1Hp`RW2B#ELARlEk$5!dXgyYZatE) zMW!n%Cv(DaU&6!RA8>9kDAyQM&|n8m`=g=)PF0s03%Rg69t*(utcQfJaLyah@cKZWn~=BG4-P zn-XxmSAJ~eJYTwOAD7#;5h}>Y|7N7|r{m2QUwlo8@^p6?D;!{;&gv%*LlBih90;$` zJY$c4zvyv$NjB|Ip&I?6)`?y$wJg)=S~)3JnsbykFs+Qj2bMGL$3j)7fn!+3(+Mg! zX}kyI5Ch`&Oc(Eecj2|mse4*_)75>M%92w_NA9ZuqX8PAlIGQ*Ev&^ElY51b97-of z^;AHFYF-51z-zAI8)&7r7iiS(cDoN->lFdLe}$}fOyR;-rm#T0+^f9i_9)o!;z8zq zKQJjMIn6R2w(bf&kuue^iUhRMXpf#`o&>9=mZ(^Q!^~9wu;NRZSI9U6M1Hj;>7D0@ zhXaF?c#g|FAQ2gTF;rF{9+yP}!~Kz-aX_3JWair1x=1%tIznk)!JwU)Dtm=ErFb7M zD~xj&^elHG2Ls|69rblQU)Xf~p!mM=EY12haL^Uct~X>Q-*Zyvz7fe@#C=xQfuO4F z=d@1~1Jyx7CI5O>3F!R1U}afQM#;+DA;T<@5PGa}n{cvvLb^wE)LV#$QPk($2bA&- zMuq#Z_};L%+o#C*xd!~DL7LL-dx;27Zy9w}`6Gm9CVp_If1rrbdQ%OCTld}_#K!J8 z9yMZ<#2%xp z)#w%#6_hM#i&vb(sjCH>0T=f?Pgj7-IWTUh(H{U*gLa$VtVes z!)1Q*@#ZR{GLLG9RP6KsZ~e$dT>PS}?H-(1tT8qA`M_krVw_?((m)~$!;Q%34(@>zgP;l(!yZo+uG{ST+V1gDr>cbrBG4?sjE;>JN zv~#$SJVU5$Zt+YnE=MZfZ)}+9gyHy3J;x*|R7hswtmUmcCagNUs}meTc<)U=mY;D% zMDJHFwM@(DjtcQw`#hbexE6-C&fXWv^n1wff*fp|RUWSq zXB>6|(NkhZdEAw)GL?a~vXI`U!?(}4v_i)0Gyn6S{Wg5BZ_Eh?Tvs{`7 zf3$4vfF(7}^8Sd&0vLxj-pQ&w-2km;D}VPj$`Nc&E7;m)1)y$)QQ|g}+|hTLbAYSh zq7_JNCRq0nPgXUYVKAev$}$FeKkN3R{x01uqAD|6bqtc;D?!wp)jgrQ0$-DLxLpF9 z%O$K@ES%z+qRBony^IQoBt41mB!Tqx;PR2~kAP|3HYCH}fwDg|#Q&Q1{0pOFWd0xA zOVvLaUESC8bREFgIu8aHFw@_?=pQ8RU+LUm;On1U%YQua|2I1K??jIIi`xB#9F>&* z^B&JXZBzg626s4tL*G7|Dni=hefKcJFK%Kc`vK(Aa2y_X^F>f zq^Fm&r==NLDjrHUcEI0pj}RJLE|apfvyX1ux&{CUoF#LUGte-sdBT)@fnq!HzpoG8!`_o3_QaQ|~m@gbaVg zNKe}EEk%@X)vsfIIQO_|utj{dTd&mgYASz)>ow%2Tdx% zP!u#+HR~yKFj?P*B;_^SbWOZFPs^Vf6FlHkWp-treDX$*Cb@NNaug8P!k7J zCBJigWlRmY!}4ANmRQq1T;s!-2Xh-N`Z2e97n!@j(V@1J1Wkv!7?0f zC_7!s>;-%s$X61K9!fo#;rkEU^$(WhDU-D<=Me$m(=XGKbU=N8AdwO=W?zVX?aLV~ z=$^ak7LBH&5V1!al``V!Pc@~zzD_9Hy#za<_!T4kUA)E5A{A%=|1_TOIrI_FOSar0+1G}bc2twVF{VMbgcJY;2u}4*1X-@v zrg@*evV~rO?xv(hDLPCGu2;*=ncNO=ZtbMkYLi`UV$BBBe66sW0Hk^#mNG~b_0ZhG z9S#PYNZwb-RJl~(kC15wFC}%*^fjNiF9xnPf&b}CqPyJ&YVBAuLSpVNaR6;0QgKQV zJj(^$zAfZ?WeUqBqacA9aUb8YYwA4@20!RGgHHWh?8K2!@wN1byjmL#=k3rw9o)+9t z5HW2n&A8m)zwMpFcJ+V;?`!_Ry}|3&+^?t!8$0Fo-m_B>O%#2@*{J=^A{Up;21BGk z?*aQY0%79#DGsgt@YZ-A!zJ!0e}s3Lfd?U#H`b1_2U=_wbXZNpXOTQz3vMBpHy|u5 z7hu8>B5lzvaCLKtHPuB3DETY?au=O>ln?iw5Sg_H%WGB%fT>J+44up3IahUGt!%{D zg+;9cNJ?F#!(FH;-#Z(^<=_j_Ids;}%7@ae2WD&L+r=e_lWto2D!I3!p{s zr9~qNm5~cBHMh(FeJ@j19Es#@!M=iyS)T^ZI)J5t9yF6D4tyoEB_qK>^D zSsq3&&ml0b62I(?pZo=2B0#;R zCK!UDfnVnc*biLx&Ym-TKR_|X!FIEcV^XK+;INc2ZX}~xM4U$@(oyM?+tsCV%H{I{ z1}OSjk{9Yx7pMT(BI)ii!wFx;h^IZTUT9qs1w~Ot!GKu?aB>Z&O}J&;p~E&j%4-;< zH({}_BqSVxh&ue)pM7E@)RP(s`uZ*LV+vp~@>BB)GB(iyyk$rD`g>P-i!*T7R)p@+ zU`>gypE@IKLL{2#4uvwx(YeWNAjFbb@2Fj6uXnW@hsw_Wls;cKs;BXzaKX)ibNFOD-)# z8hNW#y^_Cx*Wdg;%S#t~9Dy0A~EW6FGJ=VN7*60 zoaRSh9a#+8ReEfw8mYyT!jx?xtewybMI_zG$q=7Ig6b7`{#GIkJEb`WUj>C7o`8`% z0`xtWL-{p$O=nk4{u?|x7;5<=kzHWF z>17xSRz`orwUXO0Lu=+0_X4z8rrlDoRd5IRuz+824Z&@*rO*Bos{WAj!S)ZNYeC4> z#XH#;Oym!WHfmEgGXeKI9e>b|K~w~%;2I_q`!$lz(5;&6#uBIbvbU1pmKoB7r6AYG7s$?)4_ zqFN~XXq+3;8+7&Esyw(pSusqgZ2iGpBHj_Onplqf%`P|iZabfEUx-~4V!*El3M2~I zwv}(3Z}AL5xPFGo#9W9K(!v;?&DExuG!NV?AU({`#p$XMMit4aUq4*7%}FWB7t%WF z`l0u!#E3zjd{e<){JHWhUqnj~xO;GUR!lgg8havgV?F-bS_21Nogt8tmBlM3`YH1b z(+)2HjT?kAGo)eiWU_N+)kOkYPZ;7$uJi&&)0y)&sUFy#()R{*$AGc%)@64^g}xCmBH-EYp8=fPw7myi}d+x$HP7VkOGM7 z4Mk!@f~%3twLzU|ee^fAS@ZqW@$X92ahnW&l+9~`-!F7aELmpP&3>9qt2|KBG zh@v#q#+<9zmTTPS!f@=j($fVOI7woUSWDTyQnqTo;c#61v_2kBGp7i>>DoDcC*HJF z)4LP6+(Vj`JIYF!uBq;X9!l?kVA)wAr}t z!YV|J+%AeoX2S~ep(#MI4aqYW@{A%cE#KSPhQf&7+gPZ-bw8A&6Zbne!+9q;_6~~% zbK$Q>`9+3v@q84=8LT{@^5)#PAGsMSM81(9=6GzM+V}ws24Tz9$f7LSLzih-?KFZr6Y@a^SLMO$Ng3+{k4n5G>Y-LHAmaHOL+q~*w%_!gV zeCZ~JM%~HDBK(Cd$*2w6Dvr-o0oVB)uzm)(9zW|B`p^vS5JFijQDvUy68keh_nalP zFHt&?QcXFk$Bnqkp;s-%NK84IvDiaUcS+EYaN)!p#RH}|$XN#0op|cR1MyjWV7U>U z-^Ou6VvnORhKH!;0wMXuJv!+GBI_v&5JUk;UlTJLG{bVfn*>1nE{ywDo4zF?H{`jK znqt!0!XSr@J*U(3kcy?Bv)vMRKsrr)nfX%hCP<){04e6}>~CTdCS@{!U^B%j=)TB1 zq55avDB8$Wh^i`sPDgTDn(*&|26I@~=Wb0!BzzYqjir;UPkq?B1{5^m{Y$*2Ag0Ez z@qKQwJn(J%lUr<`W}EgVC5c6w!7MLpBwU}r>fuLx1)Wuau`Aresxczf;F?yZA*Um2 zF@b=LhST=@mpv#Xf1PbZk{i+vKwnkc_-|Djx;UFoJ(qOR#B}2?#(Bu`iOnD$kwhYH z=`Ak+^0iRI-8>jZZQek0J;!0)>AjlTzVvK?a0bTaU??b^603r9mCUqW8nItha6Ns` zgf`oqOzvOR7S0Y0%gR~vAH zGx*IF$*sYPCi9rZJ^zxN&Iw>g|K?Iy|LNT9e{d7lKMt}g(EVQ-fB6evC>T51Iy)E| zJ92Z=3E5iNIw;!d8yf%dgW{jc(m!UD6&e2Um;UsWczNkWzcj0_U-{o#P5%`)`Rlmq ze`_+aG5#$c`SVWv*KgIo;SpA*f7vVkcazD`5wYp5-%<}GGy#(s8iby-zmxp_506P( zWMM=JXL{LI`7sDREYpq2CpW{u+4=cPV=AfH#>=>D*J8amI(oI9PaKzSGKe^L_tyKN zwY{~py-GM8Bu~wxaS%+h7TTz%9uHX-ffLD_bCq!{G+522bXd+=f6mdhnFPP?y=ryN z<9UDd^ugug%+=HG^Q>NfNR$xCL;I|{eBbu9@v^L{Af8tLD}K_>XyrW&HDA0>ZcTtd z9nIA0^Uji7;#*g=n=2##z1q?*aRk57bYaNLl2-m49>vf}G=KCj^^^)!ts zDkN4C_QS~=BQFw@UAfOR_^DXiweQ-(_l$xOjD8%jArboL|KaYPVnliWE$`-T+qP}* zwvFAkZQHiB+qP}nwr#tof9ISzIg|V|XWnFP=Dm87x~NL3DtT(vcdgHgxRHDnEKr!b z8Q)wN`+-ZY>t0HwDmkSDpi^QAM*fY-ud|y7iadpap*JN3onWwbexfrW25={#SivB0 zK+RA+;P8z%bFQdb`fhtWM3Osp4=ieSnqSZXlVS;*xj8N^yUC_(llp9|%IG#Y^Q^jz zd;52`LryZ&RnJ$^D)jhAcexkVX|eIu&DQq zT6(C-;8f)J4kLWRGb5g=NuCWj3_BNc5b%DQjqm{N^X)O$y#f1_6KvMM ztEzsuzgB`C4Hw{uj6psiO5wE#FLHVX(~zmGWjYp!I($~A37+rh6kQcNwd+YY8!C?! z=_uGf#xBE{!(g6DvHhk`Vi7&Ti5DnMk{z;&-Fh;1_Nj8xEl|280Kw(b;YW&Y@`#EF z8?)d9BYF$cE#CZR)mv+4w=kyNfZ_am2C;I`w3hgjf9FB7iFDXv#UPBKh*5c{q-IHe zm(W)3XrXvUWjDQ#^+gPkB-%Hv&q?;1)BBot5d>`dZOL#sPQr?MmcpSRq^zNc7@%^A zP8JP_Pu4imzATFw#hyU;_+l4tEE10Y!VwF>wZoe$mK_8=FmPkP$O*R`g?Xse^q`~} zf5%A-4QG)fGXG^k+OffFAnKo;!^~q4m%G^?tk+F1hdl))2o&NY7(g?jt!yZFrP5#j z0F0&IBaO8p$>B)gEf=NL%At`-<8ssONo)n=HxPnu31Y51m;WW)afJRC<`oS8MvUB* z(q)SU5aXVwvy%KIT>}bg57Qn75iuU=(pKdKuRH0b1l>AH`?-8hR@;gI+9wWB9r!s` z2|max?7J&Z(-iQxydPaDuu{LG`NfdibLuC6E4&-GR0W*f2=zV_)Vtu7Bt|4X^smsL zZ3}3uUal&##h;y;3Qg{pbVf*C*8pZP-@^EJ7Mj2z5i3?$DsoX}l!aI-Q9QCQV6%?^ zBI<{btY^qj`ccEqy>6lEi6>CQ)5xeRM>w!4T1$C+)7_(2JKbr^eu2huB5xPwBvRN* za90HkSsMQzaK;R6%nHfr>QY+-WcX@VW^H(4V*5^^Etmji%rvD`YBM4O8`#BcGKsX$ zIpb7~bsaH(6(UF;+!$K{53d$zjkmg|SY*bYC$7g8M9JPOA*a7Aa!?LsL7+Z70~G;7 zQJuHG&`+YgIncnfp^75C0%c&BDyVe_BEWw|>{1CP|Nm)$J^4EW!D(c(AdA+7` z5XQDwyE8-Ad<K)1?AtS82t#wxb7(W|IukM_-mr1bvkrvwEmAfTnT!3ktEgSWMOB zl&05(6&Y1CEE_fm4Clztn#G|Uqy2i+Wwg7hx@V)-88Yv{#VPFo2_F-t9;Y36K`T%z=NK

se>Pywc{%vqO}wK#$4J7cPC`hi5pooM$g_z48(a1qY}&0+fNqlPF* zjJOr)!5``IBjY21lJapX>##11G$oC@uM(D>gVVmoas?@)h$yp97Kt=PgO#PJ%l_gO zl3PIA6nlE6>=Ye3|CL)IO_HfJ$E_~5tjK>}5)Lg-j~R3IP|!^VZV*}-lii^CEK<-1mux}d-!NUj>z=%>WGR*K8EPunq>t~!O z-83b9cO-Z{pxufca#JDFn!G= zf%!sEr>Z@pmNbNhRI?^o^Z8b;==ge7k9+KDnQcRB^!X@ug!Om#Q;lT!xF_`}4`mYW zB~Fx+4wQ49`O(6sFq_!;;V@$eKCoIj<-M}`&;A9)e+TDO7qEWp;NFoLg z3{zH41?y1RJV^!1lDVT};LM!qp$XkX5St6X*n7N-$s*WPUi4U=j4YG^pv@rBMoh4B zsPskv=|6*^CTNW%fA5V*ZcjL=^HYkV*1D`6QHJh7Bv16%1%;#qhN>bZ_ox{|nu>Ge zrMXgdxl?m^@9}?LqKJ)W4o7oJGwo-r+g4`EP5UbBI)#}bkhWK|{W#-0I1{cyWf(CB zKVN@tj{~;G?zyAH9#CXYv0{z|a7F_?AmESxrDVkJD6uCbc!NeBII*XY-0|f1#yJz^ z+|i5d@k(t`Y7VGsPQa;-9BOxxHAl-`A^3+bT>)$lWHra5?-Dx$TJ15d_pUXkI$z9u zgPk5cw@1@mfvKS{HXpg%l*w&GXRz6W&KF!&vk zWzLB=hKV@~n|BO~9}-PZsep{~V3P>psUVnSgC;>yNy3<<5{#l9;$czA0GKokNM!ql z5_IRphcFtRV+5EJdg6@u$vxhh2XCm5Lr|RIP@eF~PrNI0p*5Py=T*Wb2kcggG3CC` zB{$pgosM!nsjLndjENcB5mY+LY%Xf>&r5I@#XA>;JsS$&OM1Y}@_|Qn+_ef=mESIT zBj*Uw^X|BLfi7ia=6c!HPA)MsOihzZpq#t*cJbvog+-E!6cZZn6ak6=v7&>=A@~9u z+w4tKSE!W=kv8Ui3}2Y*Yx#B6sm*gNX!63^(jG zP1kH@jqAZYQ_N`;;?<#;p}mI}a*k@7SU5j6(>DQkj@y@$x#?H1A3F}AVq8XIT}&r_ z_d3=6Hrgt0S2cK6JXgs^l{KiN)Voy&$}K7fU0H1@btI!Tm#n@;jhF1o4Oe^g=__;N zfnD^ld(Mme1{n+aHWQ5odCY3E3uE#d2$GbsC8YF&FfjS)1{i7mOiB5EnmA53+sQ^= z3FdCMIgi;+vplh0u|*O^lW$S4+od(R?Y^E8aGv#-wxNV;fS6;3Usb=F-BxU_wV@oZ z^b@?{hq6a_g4H;d3?CsY#Pl(?eB&qVCQCqpCkI(`J_tBQu6f*W$0lKWl$6y{@?#rwhw0H0m$kpLg+G|R@n>J)_d@TGblJ{=lb(GseTS&U2;r6N(Pj)t z^#Y*f_uj6J*#>7f-j=u-VfjLG_uxEWX$?!;gVYe;DGh5k&#-YC-=yxGj?zGsxSzstg*L%qeKz_1^ln$c zZ*JRbKX+<2-8XrvGufK0x9WU#s>#cc#tSYfrOjGv`uJ&}W5zh>nT7ikB*)k*6zeFq2ylNU9~G==ki}sbD-%*btO6 zMY_s+H^JixWDnA1ApN>!Uq7iFgJ6KGB_lct&cxTA#76JMQ!87#Or@cFN#rYc; zp9$C6-c!MZmJlFDVkuA2j}I3VIbtVDqmVu>GLcM4CeN(B?$lFpGE}j$>c6~K??*c( zOU6&x+eURa7LrL6S9QUjF~|Z(({?qhl_+4$@mzP zS5F6t+WXxoKg*|)!AR!{JShaKgi|;_d$nn&)sC4NSW7iUPW*9fC9CYfNPtnL0a=2> z5%;2oLivMkrXjMarGo1>fWB;xsuidZc||~MF)mA%u(o7uMqwRPJ!_*4wa&}ILPb|s zOhA8|*gQj{)7X?QRJr_QZW$058O3P>Zezvvs$|?=hdzSINNr}OKw{zUh=@E76RM6{ zsSKs1k{yKi-YV6H}7?d5Icq1m08tFALX=8pnu^WYTP30cUr zH<3Y1DU(rV8tej#VS;Zr%aG6YB77)ZUGYS|))}6;o%O$>;T-aT`W=!e&A7cU^FX9Rb9q7X-_S%YTt zWdH39er9iUm$e4-UvEgRvA?EBK`Lw>ApqCt43QXt8D^w#ZM>PmW{xvffj;H%Yuu3GC+SwZ_ok= z))SvEZG{+c%(hEt0j{c+q4Pxi5ijNeYz!=*J%QdyJfl+j#$;O5^5<>9=Yn?1&~hwH z`)IED!|G?%gO1ox&#qz0GhnspJ}9{T#}WTTX8{b#sdU_I5Slj{HpqvgE*di5Vrxx5 zO$xAsXJd_kPUIVhRc77q5mL3s`1dO_z!IyMr7!$<1~0j0ua^>XYYvFVp3mA3^-B;R z#30fB$J>O8l&wP#Il&!9*#N*8A;`F3itvL@S#5MY*rf(pF|U$ zW?DC!(k^@&qP|zV(^JOd*D;TD#rqn5mDjZu0RsF@Nr9?k8)@f{;h*8- z`EE@!>v9`utCZu!%?IqY%Xa6Yi=QL7B~>8Haf7xla#a;%o7wNxpYJ=qnT6Pdm~DI> z(sow(VKvEFcjNheM^shiZI9Ix`_({`Nt95xbR>w0n+N||UKZxh;)&y_k%tM%$rWq( zlHe(yGxmcjf6lPJ$Hh9w8~ju62zz|WHQ^%q*IOrDk}i>fK;eQ6g^U4FK71c<=_P0^ zWH$HUoMe)M@72?1mE~oGSUm_%?>!~&1I!0bk5KU;G4Ep*pX1G}O#-~w)YhgULoQd4 z8*@kkOmkJ!6_5;u!+BhVp8#`ZXfPe_{-S6 zc}5djR8k4rCp-*Z2=;Q_AbSY*Zq@$(7N=IE^`ugNP*yCF&BBPkM{x7Xu`GD8mb77yoRY+V2gA zJp7mbS65h-`-P?9h9%yl083_=Ap4sh6jfzZyaL$(Xr8b_;e7#MR~EB6lPP9@PCK^~ zm(7qvq2H;*l(b4Zkh_kao@xZ(a!F8ZI294iq7%|_gt}5Bo=@1e_5jFGQX*3iLSu)# zW;gd=)q1V4K^OMe!8l) z?3*$rV9w79yfE0Vtws8|60vWX}o7Zx^y~syOIT1qcy)Ywa7eQU_d1f#^1aDQ<|A zoK6{Ec3pqF-%D#HqiDtc0En(HFF$@pV=I=3Nv0wL!(%-3EU5o8T zkLFM|EcKlMH4@L)SGb=0qlJ`xw$`Vvxm9Z>$gKU0?c*|eOPATc*&m;uZE&^LihfSH z;3^K3TI-d=j|yZq(HQTii3xG;0SRAw0)=%-`E+2=x&EYDrXZ67oOB8?Re;7TfW8Lk zxrs$#4smMxLIqpq3XEtf(ZVhB#i*dEzG!yc`P1>ev41?21R``N5fPa9m=6jZogl_4*9d_+ilLzsipfHaK+JjYzRxJ zF&JY1KOwqUi_V$~*{0QtRW2FLvW+WDQAv!M$9XtGN6j!NN-IW)+{pcG#Oz4>TbO-D z*NgiYhmsZe8-4)#!;dyYvdg)(Opjp;gM{b#bMymHs=Qtq1jaqnS{{XncpPxCk8@b+ zOhcELQ(B-cllu;LpiNzah3wo&xtm5UUM8WYY{wby)inN6Q0+>z!dXb6q;+MLU zmbF));N{iK^kye_`x{1&-zb>G8qfSp8na+V-|5#PB=P<=pZvmoiDL-koR?GLtzA7K zf^tC&?hvqjqt5J%*$DDI|JJmz2Btp#dc|k=3M{RWt28#+=% z5B+1dF7$^kV`#;u*2eecK`Ogax94f;ftaS9L&9*Ho_ME4nGVG^f=ImqR1+twQ@?4%|&_cKh>K zmPWf$1@)=l);}FvEPZ^23ED8CQf~VmG~f&2n}7%h<@o>7DmY^ zW=@i{Rlh1fBVlPmMeQtus*5`03QQNWy(w?NjGVv``S@Nkc@kIAy@b40B#c=fpM?BD zTI|JbIPF`DVPeOq%~1L~m<`a6YQw&Ias9IEf|)>)umFYV{|z4qo(1%LPOxbRSLfW? z{u-7+EV2ImnGqhD%W;c3IoE|zNlU_0bK;gr?~u%V0Vm{hUmzK| z{E~kvJUL0$cq~2#T$ePgH~M(Fp*=*2IDfD{&J~NOvLFv9BnQ>ViYJg`R5Bcw;a35E z0~)!6GSHG_RK;rW{ixEriU1v513Y-V+LoZuEFA*RnIp+oAn2+xn7DFzO2?a4YeAo| zG#{;s(E2J+swDgWX8>?TO1H6K3B0sm8oif`7^b+~AvK6EX+h&`;NZHfq6uh|P;G0D z5Mvdx>rs?`{^>Lwm!ckA7UI*5;T2zZbH2LNuZoVIao0AtZ*Q9V<&c&z_*#}Jpg?$q z9P$Bj$mQQ6kCTKJ)~Z`A-V!!nmiqWZTgq}{X}Je9-_yqyd2-?0myijPH+&7ls^dkO zu%SAXDADklV!;qK-UFRU2S;~t|Gtb)F46t?W&hlTWX~nUID3=;7I~qh+-qpysDqJp z;wlhtlv5fQy-dr*Yd%E&N0Z>dcjhF17*yi6htCp88VpSpvrRjTrn^^NjmA=8HZVFQ z@`tl^_|K>E#&`@CX%mo-o8UiKv83rw)BIuv-z^d&s$@w5`63-NGYLdGWNI|`-e8ap z38HrbLGD_7FAELxn)8ax34wxm4zJ)2@z!vnG9ze7ANKx)AkYsg!Ta3(>A9{xr z2mAXCHf2|2b!VPwJ0BA+daugQC$mDnugUSzPBA`g9+uaU3QC_zt>K*}n4KI9PPf!_hrHx(bod zkqQo|phA9jOO6=Ln15#?z|te)r#A>5-cIx3OYbAN<3Jk&90`^>YH9l{2znZ1%Utyq zDpU|dy)K$yeBJo929eb}hl@QrxNO!*Lbh`%7cA9F&m0e1oonUKk^D8e9o=3pm*)Ma z8T?zF94m@-< zBMM4Kl(-@glqXitSP$485K?l1afvx9a`R;5B%C`j#|1>Bs@<`@4GA+p*v_P+4M|C! zS4hHqoBW;P>$2+dd2!6S_6KT&ZeXl?At(TpzhnOt;rH%hi|P9XNU}fgeo<;x)=HxI;!Yz`|@R^$vM<}8jU%7Z?72g_>*H0hK-jBR? zUp;bPJ$gE2C7!F)`I>9y$JPA42`F+_1d-4wZLi^2qjTFn7xA}=$KI|+3u}3Md)qtt zF60fLs>^rru`#{g7j8;-Js|ZR(<}Ca5COp04!PfiGs*E`1(O6OKw>9O}CGgnaVfbklg$s zqt!Aq=W%f1wC%(ELWonQ3A3Uc$|$HP<5`cBUV%;O<2V$s8=|BS)t=_b?1Wn)1PhV0 zxO}!{r(Zz0++4x5wX=2RH9D<)1kU6h-nwT4cYO->&+yQPBN%CUp&4HL5{sMz_Lwo; zoVYLj@N%=!K5i=Q6x*oyjRL=$&*#YquY?onU;zo}10(T^M6*6G|qw1X%SNJpvE{xS^QG_q7L6EQcRvmZ5^bvX$ZFv#@*|6R54 zxs*aH13=uj>6BRluBQ5Nxc-n+zJ08{nsb=Bs1?$Q8k5gJo)(l6Fj4VU;%et=eY><% zkF)W*M$Oe=$$lXlwB&Cc!8-CcSf+hITurOS?D1)2-Y%MriydSgXBR-v&}JvAU84G0|p~14K)0- zJSWR-@nHF>z(pJB-i2%qbOm<%p+|Eyq7F!8YyjIg2RvWJDe6^E599#3>qsrhe+t|7 z9%ZU^Fb{Nb$*Q>TaGFB8gH^hNT{;h$EP_&69+}KnMDs=sz#_>KqzVhLQnarEvF=}2 zw?L%eIJ~kR0U~`LF~wNp&u5B#VJ1c(_@Wd-d0wbVqkzX`Q2#!Qm?_+i*JUPfhe9}& z_u^XrGd5gI$-gZ(6sC{QM)cUHXB=KXh|!Dlw}ZxNI%#G$o{wu9b*&rtfy^R`@5zce{oC1Uo<-9NEvFtihZCBuS#6AC0vOy zRDWrn6GvnLKs*}Etpy+zb|v!4GXE$l9Kj`uIAL4bicC8!;RppkL^xKd^i9f8p7jtZ z!_g+yEd3%yPGGF6!H2vb`>)^97x1CN6o0#xd{BOKw0J4)izn^|RF<-nkPhaz85StO z5R$WUAq)5oS&UGK6at(isS{uzBW5;92~SJ7dS6GTd}O>QpO9+QL%-HCR)T=nvECxw z^o}}f=udBSV~xvGJAe2Q2+=58QN~#cl*34NC@2cSQdt(sSKuHQ@oQ+Be&uOVe@BuiW%{7>#3kkm&VN;hD?6*njSF~0*RV|~!izdLWg!CwWS1*$Q36QElhbkW# z(?~KSSjJ&kJy+u`U+Oc^rCD(E!5gAB!qrN~&WkLc>eoSv_s8;{*pPMaJl-a_|LtgT zQlu^mkE9298YzzYZw*38Bj16A*_t#O+8?ilY=T#o)(I?S-jP?2sG`{3V8XK9yC?4t zk%!MTe8=0xE9lBR0wkGo0MFsLr$zO&6B@MS61H4;mH&+@dM4`hydSBFkus`yqZft$zUt7eAP>3iI;9 zVm4uIZMDsKRRH?cUxCx(BdJq?k2*|~sd;?jwHWnJmMNEwf*$MmnT;TbJ zGhHG2Bxx{?acDUXO`h9$Afton`B&wFY6m1$49BEb!En#Ue zWKX)K4Zv?JFOyHBKMmmA4QllV-X6t#OByqTjb~~sz2c2>U3%GVzJ(pnz?CF*%%hE5 zwcA;lIX|0Xe(S^bI46II!Y?`4=WW;(f+lo|jLTTEBv>uwZcfL>jeWinE4*l$kxLpQ zl{S<|9dvKZOR*DJ4wjWMw_j>;rj05OlDh%14O?YoWf_~!R<-Fp&vd{oW}mjEt++ID z3N-{8elWAU9B{L}cmMp89pNx(Z9{lAi2L`K4e!P$KRjC&j3!)I4OHL zUsI6B8bh^_ubRU~DGpbzTdQ7XO(&Eyu(46==019#{n(>*$P+=`2|y!i@v@|XNx~!+ zkz?Cm89bI#cr+;LFb5M>(>b}yq-?t`M&j5FtPMGxfg>P~psiF$HcXDpk)OxSUXR+% zRZWvlR75{h3CZ?O8hnf>Xc4iVJ<+q-L_}BC80OR8k3ncNX3v)%5 z^@eRkQs@c~V|Mf&HGO=qoN~_Xz%}IqS6}d-!z}^oW$nu4)AJjf?x*yFO1jXkq=>abO?0H@aU_0=f<ne~%+C7V7P`a1u_WM~BKvlZHsj%$+eaLYfk$!XkIBK++TnOV4C_h9WP5(E)z~%)p;^NX(|U>Q z(5{EZd^u(w`dO>_SR> z1%W97l{w>D)7Y$96e^dYP+bcJsvxVBR`k7g8TB)&YE#cJlyzNMHYHdm>z%--QWs{fJ z1!~6Xc1R@S`MAPjA0ovC!+aR(Y*MnisBQ*gO zmhrB0%FzXW_`O%rJ~}R-d-t>Xv$ZhNUu~t1P;qyR0_P{n0*$X}<9%*uqWj(-o;qI=pE}B9qm_$&k-o!<0aaBkll0Ncq-tJ^dqsVP zgBhpA7IRbJ*8FU??Imht)iD{MGE3K$jH$hi4D+N4uBh}04M zv^Uefs~$pJXHNFr-{1r)1uiT{i!M#GUi6}=!4Q4SNi$-Qa+wCdncMSqP~TdbxV}C~ z9y92vaYxn}+EDoEmG!gi#pWFIGNeQHan$~+{jV}|CKY|gS7ODj)c51cJk)Ele0Qej z=M-6IpM@*73+Uk)Z|*x9Kl`157=W(#9?&`}s*-W8@y&5TV(1EE{zR!-i1A=4BN@gzYi7wc zLieriu@vcQg4b2G0AR* z5R{`Kohy=w!y$T(d{{daqC2r>+qs+pkpg`Sn}UoW2WjaHhZ|XW#pd=nA0a0$N8WFOXV`8X&8z%35D@x6J zI!M&5HtF{Na)AO@k2q4=Yt3se$#d!Shc*jjgg^zwqh33=v#cdp=+K0&M9w2pAu%hD zaSz7Ekp0$5c?KSobzedO()!x~K}tjg*c~+)^sKx1VY)WTmC)Q(^_ZbvHz`yT z!Jp##qlbt~4>jloL3>xR++5Q|L7D7KHMD@f%$N_rAH;a&uD4kk}N4bLt zfpC!`MQ|SjN=%^$RHVBs8A5p|m}r{fLRbMjVnab$SpJ|6FW80RTtv9KszS3ko3_ot z5Yfp}Wa#o!p36*%jCcj#r}}LVm#8E+j7tzjU__W(SQtli2%cbRm?FDEg%blH&6iV} z5ALUW(PYzT2Zpka786B;wk=ty5wEW1jv#lG2_UsDZ`@axYK!oa5xDnx;NBv5RJVXk zbA^iBQbgc{xbrz`p0-n`VY<$K_YbQ;$v({4t!F0RG5pEb0p@u9o0R9h!xJkfN))ZH z=}|N%u6Ep5gm3Eft*xzLm1tx9yb$f@M{M;|FIorv;S12=PEgRi)VF{|giD?}q}(or z5Xxl;l{#()m5aXGpirgT6tJR$KQCOT!&GG3o%%nlgkaA2Cw&($Ol>T}Oeo}OW+}9O z&5gy$#fpn{4GQ*FckFe-^a;>G5sFjAB{WBnAfZpi$p@GV8NU8S6M6hw_(%wo#M4XQ#{jDra1FC%c*`dXA@6jg(_2NaInxr0)3Hi^tKA54M-vGw4(f zgbn1xKdhj=_kC6lt!%dUQ(3%}dCEI-aJ(N7yFFway!>yQghZZB7K(5ti9&YlE3*S( zVG4Jp(*yNF*-gVKE#Ej7Vw& znN#?D=Gt)7&H~O$Pyy0(!6&rcnzg4a{d_JoT+Z@#%SV^({=VKc8gk%co~;qLbzu8| zrWSzAziOilKR{-S2X90&v6O#>a7xzUFqVsjL2#rPOK2hraL-2Nard(~p$-d6X(T;H z9Lh7WN2ZR_jG_8PMJULm5R5Ki1Q2&h_^Z2AdJ`;s*pxbbUpn178z(WI9!g3HyrZZgp2svt?E! zLd#4kX^}71*Kb^*3C+utT>it@@9wiJ151@glpXG_uUFVyhVS3KoQj`6K}dZrX!(6A@S;@BrZ=;siG zDwUY6Rf(&cB80RMt=IxwIa7#efT5N*{)*t;zHpuHpf|X{EKYr$_|5!gJKZ?`yn5?Z zjG*9aEVRxBwF8G7#m4xFuhP|bUdi}<4Q8F?vmqb@e!q^8@6LY)BtlY-_ZQ7gps@se zU$yK_GBW{y6RMXIF}+zyL}DdYKbAfyFKy>MW1CjYRkyR_Zoe{Hl2t65AF+(H{J6q; zB}K+yd4MjHtj8L8Ly5z!@10g26Dn`gFjNv!jXj6)0OHJ z=i0YO0ZQcoCWSRPBb2H^yjp}W`p?Ol;Z7ua=f@Knd_uf$kEHBT*R6pBaYP!OV0c9z z#3fcmmDP>&2*wCX0)NApaYNR-YEj@t7!^a7*ou9taL&nw@YiykiuTK9?)}b8!GuHG zPh)dZBGF=wobJF^?NKS-Kkuh6&{~lthL+lY_iTLtQsmjjHa5NS1LK^)v=xW05nEhF zvio|7*RWy7Cb%O|#e@^f9CZ53-yZL^5*LhdjcjnBF=fktO6CYw8XI~M>*&kJ>ekV3q(9{Cp57^X(o*gb0pZ_^h9_U2(G1Kh&W zH@k|x`*#&X#q2uT%};5S=V~Pko`gs@XTU!s&CEu+{o)o+v2aUkg~PgKrhO8l5YsH7 z#uzkR7AoCNWa3zBt%~!I^3v$FN-p3F@R8RO=A4oCj z#ItNNOrw%lmah#~9Sm`Y1ylp(D*~k>n7|fxy%q zWEwjJwK3kw1ks8mi)du8QTP}=g9y&3`Xxz6B+pi1rMe+1!K~)RPy9EjD(h@xIgZzQ zob`!Aj7wsC%|u6#t!KNHIjq@-{I{Np0KH+9@_!^S)m}`VwUMg==^WZOrBl@&)^;XhI_!yIi9Ll@6q&} zkk0EVoQoFk^qrwag9{V$^cYzy-9*m|fY~+VcpEvG?FZ(0o$6-!Emo|>>BWY*+o$!5 zbJa88XQ~sg4%81!7R!kF!bo$}72I~z^_-M8p(mU|!p1n;r@pMsd|=KEZ>10I)>5O& z6?B&=nxdCeOwl{8>ru}wiR;Tr2f8}ZaQ9HNKK7pykEl}xM4WkelY(5Y=qlp*6K#*= zS+-mUcq<+KuB0#50kZ)lk_zy;kViB7U$aRJ@#J7L@0CQ?7k=Y)p}65w-#*?jMlbRc z%OTdr)Oc?-&suOOY%k06nGUylX`>e>Zn*Ss?*TK|R|>#}7`G`Wv^%U@qV1KRiP6_U zuCmhx!7J^yYUVnLELl>@T1`$Mnw!w^g(HfiNIun) zL_D__>Gv3#W`+pm%`<$NTw}Y$vcYMm>(iQl)ZnpUaV`A(9l3^lv52lIr@Cca>sq8d z?0StqVZh3zUsG00Z`{D!qh?PAHGKq#Uo_$k@pqQSR_oG@v3~|OW`KrIIltvX>e zL)Uv_ze9&4Eg|9sJ|vJZVsu;#Zc<mz2HpqPfqq)97>A#y5-)8qyf z6yJJB9IC&KX()-TmK6S|4xWMkRTmD?D6JEM>x0Y8FjmS0(nGiq)h|QO7Gf{h|IHnK zfxj=)iN5#NwZAF67_BO#=!qrh4W&5DE@}KmP8jheJ5=n6)U|$1{Vh;cd-(k3`1Pwg zYLonEQZEwao@yi-d;CVS>O$?wpNJ!#xzp|m@n#HvMkJqQf*VSFK=o`1;D!flXeo>n zgiAy%igPA?95YnpC&L!K-%cVDwcX=rOE@)P@f-u~id}t&Q=AWNj;EE=XS_8dq9lD3 z=f#wUn&we!@TJnI#aTT2LCW;;^U3Vs^TnrJ@jD}oA=1GaFglnGL#JyHrPW7sY0qsN z`vUz)ie(6QNO+?u(IAhLD*5Ge5&;F%V2dLmGb$4|(J9G~#u(&CoKu4l%t{++9&x%p ziQzganOwoJIYqE6-`VEK@B~o(X*;Jd?TF2P?0)%dV^*G@C)I*F*JMCYS!AB4+rfV2JDrb+X>fV{X!0nv*tn^g z^;@TdKO>uDiRrb|58H5~I1p9f z_Yn}-0)E}ma)@k0MHRV8{Ceyib)p+B%yH`8aZ1b~@7;Z@xt01<_>tNA_@uT7R2)Cq zr7l(|U#c({q1K#_P;Dwet^Tj>@BPLX`f>oDss(kj%*&YsCzLdo^nOwSNDcVZ_<>>b23)3ax2gL2R>D`a+Z>flFYJdIYx4xW0IEd#CV6o zaImRLR|2`*aK+ptsxQ2Ul8ZQJ_1(lG6ER-p!E;F1Xt-U3JMw)sqFsgR8Bc@aC<}Bp z<|6rlX7Q+XlzUV}>^kbbI_56+J^Q^6q8*Vh`+*~0J*8!2gL2M9a;h7!pU z?uG$m-BUC$6E*W&4jJ)v{Ylfzm2v7^~S70CG^NLGtDZmtt-IF^e|<|Qd1lXZt=bk$;T_Mr9hHdgmwKX`8VU-TZ|n=xAL?G^&-vJkm|;S8mdf-B_hojD%Mdu_&!5Dk&=|s41x_CMhQ=q$s5*A}J#& zpebRK2uS%g_|xU?6}pl6i<1}w7;_W_F~u;YC`+R9qe`L*qKcyOP!vLxpeP3^K$G&5 z2$TAg1d;-h0F!=_e3N>Tc$2&(?Ih_WkYmxxE=J=n3!gjJFn zMXkJUNtduk{5{aI$AnXo8O5x8P6@Y&N5VbGvBQK>k{Cs-JYEU6@Vb;o+&#dt?}Sg1 z7sZSGb;*`Ud!i$_3H-6^gzW_FgiVqb1s4Sud8i}wvFU_N(p8d8QgsqG1)F?}d`+pg z*t&FkiX-f?<%CKS6@{8SZK-vEwrG1&L%bv8vEhVB5;O(6{7h+<7)Poj+_Cip*n~zB z6NQ<)Y-yG#N3tW-vGIiG1m}bV$_U={S|O$vvr(++<$M%RM!gu*v-$t`B_tWce<%C-IaMUhIR`PNn)DR z$=+&wigfXhxp3=<@iTgrXe*Su1!E?(YMvGxHE;@$l(cAoe*HK(3N;WK5NsNB0W^Rw z3@tt8AjktC{@!qc2R{$}Y7si*2>@OmTp6gP^t*ikb~bbwSOowXJ$4%C)KCxRSqbo> ze|aBD9#R^NG!UhKI6sFxSP~Gj{~bRhJsbu!3b2v%hIzlaev$+>5e8%wy~S`KVIr$4 zJq|fYs6h?DM<|d`ogFoD^VVBR#h~ z>^M+o&<=p?09p19qB%gb7Vr%~8wY3XzJZ<gKPCXin*a8X#^Z2MM+QxerhnIISc-f{8)hA z;MB9hGhZ-YAUK0-F=sx2U10El8|i!D{#MdCfYrJq73W$2y3C^Sz*Ol4;{M1! zXbf*WcQ|)6cQC0Ou$+Ot#6IM{!#*SRd-7WdFnlmPpl2X>Ab9=bud{35YtZZdt?fP? zj4tFh;5FbaxXY0VoikftTToXZF6je=0EX~IkX8UTU^c+b0GmG5J<&a|MDpI+C$ua_ z9(&vqR^U~jRRA?$H9$2WHGtH^IvMt3#y!O;8xUGRS}@vyQ0RFbfGqf|uy!Yh368$? zv`v64&@9L-z^s_$(tIvdc2IUqb@(bKxY9l1!!o2Sh$@yCECHgr5q3(D%4i{9%nOx~ zcsg~+s=nvrwN@B)OB~vMBx^mG|C(C-2oQVbd@XbUC)i@mePM%L>KyQ8;3q&%0PxSt zfX!`!F(OHZ`!I}Bwe!RZu|cDT_8^S(D99O~p91$sdL-q|FDpR@ zr6&;sf%M7gK`^W}Cw^``5jp}!SViao&g`u7#R~D~R@X$K-Z8X_33k^?pgfIg(B~G{ zIw0@s8p7&Bc-3p_L%wZJ0M^%baMjMhjQ8tm_|b7zFwa1oK{zqA&N`Ry%)^*A52jez zrv@mh1Een}0Tu$72~fouAelhMNexN}#)+qph=G2~*CrrM!ys5}B>FX=S8$dm++AVJ zQHM|8b|9A33(%rSlw3}JhJwAQ#5y3P-!He=CZR~aV(8zZ@G*#`fCGJYg`|D{C8r|) zh=@Qhtrx-KYnrk)vCpE@-S0^4hn`)hUN6Vm$&2M(wq2?gwfnB@l-9)?mX}}cq*{l2 zabs&^Vk^_ARfg`Jhe(&9FyoAS@>465z}GQz1)s)G!0sJ(x8A?6z}!1z*M%QMKKmQ5 z8aI8ep260~-u~6CkBAGpJ5b(Hvp1RZ#ZQTI2UG6frjKcvjEBRMyl$f>JzYLtpXip{5AW7wH|&;{oE@%a z{0PqqwM#3D*0#yCikl|IvF5`PZX0f^0gdf2R%LBRh$oZn0T-L1nwHWsG`^6442mI0@D-6wX{!5<`T#F z^|B6gTcH2P>{!>_=4FE1PLcRV?^R zgd&mCp(_EWkld^Ot&(b?RL-DZ_x94U&PJZX?rpsDxHVxRg`VVm z>*TOWDajcDm(a>d_oSloU{BDZfmi*w{WwHvaJG;c+gW%OVYRVJgh5nN#-8J!2_z&X zN?WRzkti=53|mhS%)D9#u&PTy6thbx1LLiL_5*@4}4S?Ym<+L z(kL4=QESe1vg=&73et1+iTi(ms>R2V>cC)PM5?>K3WG_Eh(bUpOsU=2Mf@VuGEFdr ziGLcJnoBrQB5bBuL`N6e3rC7f3_8?Yo^i@6U8)bTwG*%LDl1-U z(5W?zA8}BmUaD$z%xZMEo#b_>w-a>42yri@U2W=$X_w}*tl>hCV6(6?kn>~U)e@$3 zLmH+pOF8;op6S*dY=XZZZd(*aSe%HzuSmW>q<&t6T%VvXzr`y1do8Z|qHcf+=iPW` z2HVa1y{CkroEX3x8K6(~NoPcnAb*`JX9k?J!Y$Z=l^8%186aiS{w~h{g~W(mw5J@& z2Q+ek8p*?RDH|SVhs0tj8!=ZlK%rC!aEua$A+je7HNq&si1>pMC4li40&EWiC-%^u zI;MXf;*cCCoXDOXZZQ8W)|VXwypRq~5CXga9bQ}K5 zzOHqjXO$4uLL0c-A?+3q%ILmvE z)Wb;BBZS(aQVx)`BZAt&(Di-hIBSGcz4Si=pU@9<|C;~_9FxGK=w)%J#l6$(<#-A=;T(e#r zd9dx8I+)tUzDe!ZX5cwZvUp#S71~xW`|3}ij9eX1iM`V#KH2m;O>nSowd1{rQwG(Y zX>gD97V80?9e28o=mM%8zZfGilbTB5-~dNI0@6;9s*3}yG6vm^v+HX;(xUf~d`)_! z*e9AoGKOpf(HNi>NTHXCBl&+06DTeMJ^_9~I{Mebb;$d&AYAccj@rJ(Td{_Gl4c=| z3mW@X&Z_KG*{C!B@8JWE_K^q!>fULtD5{~+w%u%o6GXkCQgx|qzDjoM?g{*_#t4OX zy!DrT^M_y7C zEumT!o>8nadDWw4Y3O(E^WoD2473+33BkzJ;Q6TAHR-Wx+{4_P z)pejPwk=}@#~%c^OZSk%QJr?zdZA0g=XPKaxc z4m)&1ML9NYC(hdnB+g@Ta-n97^!-E=miF2b&V`D~H5qfs(T0G52?bKC)kI8(E;R`q zP~Iu?Dd=FEPK4nNO3wJuubHK#J$K=_oGZu(l&sqFc>hYnq$EUmyorGjr^b9ct(DR2Cs4_H%A5%2Y1)E_XALNn~W)mgA4z z8NfPGr*U&tJWU3*Qju`UB;lK=?fC9di#OHoIBq-D%aK<)sS=Ay*gtWJRJA+^QHTvf zB!;s<+p~ZGjHo~2!{S8-Sf=`Jvrj z%wcGAgqng<7MRrq=oZK}C0H9`qh&cW^Ca(aHR8?GDb4o{?cZqTA@;%sFpLV)l7hNN zLOVxd2F41^>t#eHi`oHJNn?>3!i+CIa&5m)` zb+K?_1f&=4xW$DoQ8>#I88wM4k!=4!Iz(4p2;UaL(g`d$Rhf?d#>`1mB!Dswc9Aq< z#RZf<2C|!^PbL3$ni`o!O6SPbtLq)b?~DDB%$t_PL#7hBaUt-92eDb5`RN`-@gIdb zNz=G+VsrKXpvnK)HZ%2u+n2?ealo10K4Vz=h{99~Kq=(FsGP>NB|~pLg?}^pZY+D1 zq=Jm64^LLsu-v&8U8e>=t}wOAp+U&jxct-FF=9$2=!#A3(M|bY|6xX4hwdB!X!gti_F<$y1 zN3gqlC>zCaDOJ!jTWTG%g32x>(L`3kbQvuR)3m7;pS&-FH&=CIt!(wE$T6w3P=#-4 z^{U9RTiRCuZ}n@a(<%SaJg9+_*-iVwtN~FI%PYSgMKjNOX-aiptYzq7Xu2~!1k2ku z803nUFxA_x8!;x@Cc;|48ewrawG}r%b3dGq4#kf4TR*x}08Ov=j?EN~&Q}I1iWYXC zLcKF~WgtfGjEdL%<$JK`K`$Gu9mtoaSPVC+70sM{&E}XsUvy4)d-D9)8C`ted{HHw zIP6|HDyNLGKF4czkcx1wXnz4dvr(BOamV#8;${_4Zpa)qwMx@l?QFh| ztO^D24+g-6g3ONy@nrHW?bsW1jf!E8E#;?0f#R{DVvkCf@`R#Uy6@Zk-8b99F?Bca z_{*?LKPVFg7m8^sr-EguYz1XQzr?&SuzpBhY#y6lWvGqY1ga2{@z)AM_FGWKpjEudE?McNmE?7Mv&k>^x@DoCbAi zq`F9IJy!A~Ma4fR<$0A2Xg|wv0;oO|Pdtyu1~1wUx!6q1<>_0>jGp2fSmZADzk}>4 zT83$o29!a-I>yQ zCE1x^=}L6Fv)vg~>B?riXWf|$dxQAN+1=Z{_xOhGog957&OPC~h5Tmm>q~ue`R2es z(7V<8Wc^8qz32L*;+=8vj@UhXd81x^;OOZ?hxOuP{QL;6y)E@l<~y|V&gMJa#YFm$ z$^((G8Vqwq`g291XuY7oiOoAAne~gnSrGD+!2d_}MJxd}#*axHM4}NY9s!eRZ$ULk zq8=(4C@j&Wz97*t1|gIJx1ja@fSxd6Rz#(jAndCoSeGErMCKBg2ov6Vcg zjN>LV!L(dEveX@RFOE0KS0tewI#pD45)|@Xp?hAuHf&iO* zcwaGMC3jWR&8+ZY8Th=2w;}dnmEgKu&Z$%S4@RidI=h$&_T?n)oI{wBIp|jsbZ=H-|i4Yb{~iK&}oZ=??|6#rT|gz5DpJ9N^g)nq2oK1F6$?srTU37 zw08+RCpa_WZth8CF|($2>cy;&DIJ#-YBdd?u?Ata8viuIm3f&(=ZUrFHtR!fp_1j- z${s3LMaFLY z?KH>f<-A6YH|)~I!t=RgOV+(5zK&a<;kNCX+xgfmK)_B@01mmv19pMK|gR#h#g13`+ zKZ847jM1!gmVgC2C|@xCz6(<4n-M!8UqIs8sM6Z{VjvM|5Z~Iurw`yvU!*;nZeVWz ztb<8bkX|23R+!$mTn2h0Ouk-!?LJ%E3T@xyEz~EPFFg9b%PX*UD4WBwPN&chSCc!c z4XuW%UEZ?RuaB5)VX=kTMi*(5nn*D8nHOfwp5F+p1Q>Tqp78c29j7z@V|<6$wuk0Y z`nv-PgUgwH3!7Noh*({2u(x$_L1XJdHS$$?^e7B{IsX z@ybiq94@kNX`_Oj+EUj3q*KMiMiy2Ns2@r)L#3Ieomwn)O&+XDy5UsqRpyzMYOZ&9 z?uAf?8e(ry=O9ZJv(7GRwmbUH%!HlRyTvQ=C#LP+-HTiFExB(nKe11E;~JulM4hou zT6Y`oOo;dix%fh)ZHn}A(G<*4q|vY!nXtIW9_^< zV-`?1^ZV<9NmS|lCUwezuLj}!LkgLGA1BP5<+JfJ?-*MNRihk#O5*7SROnd}4Lwex z7Z?l3fW`(h|uw2?$3M>Qa(msi~pItTZVY`dKHHF#Q6;LkAp) z*C_Q%xJgWxDoQG)2_Fl*7!hIjqHE%Ykux@s&XOC3<3}KL5O>Mc6KcG!!?8+u$12oV zhkru*J>MIE#kca`(lWXL)3`HxSaT)RrDIMy0f`q-8chU!M8T$DnTFm#fs!V7oH0TX zZtrxrhbUnvMhUc$8QXBkrpu#Fas(El%$PM^*rDYs4IM5XZqssO;mem*)P3&eZ-YFO zHv{kT_Zd1!MH8v9ST0!e9_&#<><2Qes8zIrp2OK?4Z)N1)lONg$>Pl?&-$_6FpB3xg%O)5fI=snxYvUM<%Wd^y>IWPwyB0A30 zIII<2SSMgYt;oA1O4}8xMf|+pHRNtgb(}XfNq`^X%Iz2t!vHFn_+$34E@3FrTcEmh z_oy#5^R8#gmWv{v9?2-!8-W5N+z@0r$=0&hHkI43?51V6w$;r7%Xyob(#2~uL2R=*=Yjyv@vA^_nWL8 zdtk_j%-GuN_Rd0N?qdzY9Xg}SiI#HYu#Cpr1lZ6~jHvF29KxFcQ#G_C6P)hbGNWDX z;V%e!vR_>jXaDSkLj(W3)Uh{_ksQLjP-x3@k|(sI*q|uN%+A1_1yGo5K4{exKGXAy0PTaNTXwgYWt6-d>df_-;MR6sb; z57i(x!!zZbl4rqDtKV?NxE?LGy)L#LHWqb|Y62GB-?gztsoh9;>g>{yWD z)qD+N`(4rtj=7TL;5v`DUvHSjpg}gx4S!*~2)57|wqDbhrRIWK$Oo>OmM48M07ldv zw+A0mQK=2BQJa@2k@|8CXWFg3bBQWpzXyCaYK?vuFb^hc9vXcM0Fwn!>x_^~sa51O zTBxAb{6H*-sr(P8?e@`~zH5!v3$-}AdVJmNg67QD?K%-W&*$Vx%IwapqORRkvZ9^& zYof{S#9F{<`Z45Rhh{l}JVB4hTf<^{;^2hZxRojhHa6Dz<`GL}P!+cx+Sd680&8Ie^Lwac0g2W!}5 zdqI5=EN?^pXV+%0;lBn{UpC!Tn&FEn3$gF%Ue5d7k2;?B9mqG!#vRr8#>tC#-1eHF zWRn-=C7SNq(ay1pF*QlaR8Aexw`;FXBqnglW~A0mVJe*p1!@~pT-74u=aMf_F~lSx zgq^-lcE-Pcji`-kv0i8rFO83v%l4QOnmMU&-SX7O=-@oIs@Gu4GetmXOA8C#`weyA z_eFwQx;qgjs8Px7-NStoUo!(HJ|RId0=KQLt>`JtY^cmA`X+Y}oAMnF9lDx|jW$;0 zeic0iFGJ}}2|rUU)g|g`(D`wUe9`!Ew?9#;YS>Vi8-C`sD;Q)oRNgo4+>xjCzP;Ut zPwx!Ro~wt==yWH|CDbTGv~}<)AOl>%NpoOhK?w)~<`&7Mf3Q*ht}m*emdpTF5LSXH49O=?e*T5!~I%u#aA9?G4NR%G z9-p@x>V=!u4Qn9MYL?kJ)9%=Xk8qutOzG^g50b|(eWTqF^Pu`Ae;3T5Jh%JUq+zpQ z?QPk@P|lMbl?ZF)Hz&3E73(bOii=50l!0R@*mWD$I<$O_%2oQy9m`ALfr9-Pt_Ce2 z?Arbw?`Ltt5WAoR6!kN?waG|5>k_cyMl%zWZfZflLGT}tN~6oPu?UG2x(Yg-rb(b9 zHL6+fw9E5gTG+Z%9e-w)=5aZ-kc?RacSCE+s=!A#xpY=E9}BfTqR>q>rI0#p?6I;B0koO5c!pO zIH53pXyEobLKM>}_VPE6g{|g6NGcCXle@dDuS*S9AABBFp;@W+TTPNk^`cpk__(xX z!}zbM5NO`NZS)jL=)N1TZh`;OcPHfv=spU4!`RU79&x@#(j@c^WPri8uHr&_xr(LN4xrmvG2q*vvb;V>&8^rD92O z?!Ir<`#x!UE`MKn<*YZ^=|Dd@w%-SVa{lSi`?);xspa9kSb*+*8=&$s#m!m+jLD2RURjzghi6>mZr78;g)`ACfNe$ryqT zS4uL$raax;?3##=&Hq`<1*H5GKPT2}PP(iid>Rh-P2=7(Yl>$R#!+O$+?J=8ra4cSn;hj0YU5w>OK927ch_Lg5gr$p+s1_sBFch7Z1vABAQ^b8U>$cv74^4M_7rjJFUz)2IyBr>u|#14_fEuZ}UH3cXCoemoe5pys3R zNkAa87WUqFWI~qKck`qDl z{bP2yHI?$e(*bdj-7ibKdcNI`rp0Ut5XUYv3tR#LV`pljen~C^LzP3}DrPmDjmwz* z+#pU#YGnZpY$N?38-1H=$cz5rI(--XT=xWq+DGdXVIC3S+w=ky=1704|AZY^4svAH zppMb0H*DSMiXzVa<;ofK)UqGUFj(LMWk`*?cL~Hg5Xl}5Op_Dmh#$>`aPgpsNB(^h zXoLv{7E!6qX8j&vQ10-VZ*Wm|@a12D&4N`%Xt}$NWOXs0J85_VPrJ!%tEyXdTGKf%854S|=R2L8F$s5i^%A zPyPjdSkpZR^pHCOwd0HXw9`Lxzl(`zHPl+6q7js_L0OKvHaaSh*Yns~Dio5eb5KfD zg1?FE4?JRad)5qv*N!NdmqTdQ?B)Z)^Lpn;GGrL~u<%CU*zi1LvyxIfyWC?ZLbI)YsJ>|Bg}Oa32h*R&;I6tamr-BHvGIkTRG>eKqz5m-TX77|}l^0!fkyz3LP zz-*Ru2_4w&&&9L%#kW8fP3tZdhW(Lp1}mB_l`FX>L)SdavnpS>Q(~&jdQBauWlc0D zjAEIg^;Aadi@oQ885cEa)w+N$j>P?rn zNjC#IU?bcOVZjJb?rS=rOB9(MZ$lgti(#}VE;g};e?@xT7#Ziq<{8kYhV#)`+cOy_!C1|jK0sjbW7PA?p`sP3htI0U8h`BlN#Lt^^c7W)PEC6R3u-X zX=P`8EZ~|Wn(s`R;LFAHhlz4CzE0R31EDZf0%^~Ye%UOdXm6uKQ$BG%-3qzBP=BSq)tH(j83 zbU`vXU4NWcODq|VwG!G$7C+8fdPrT^xmu~3-|n7CLgFlDSLJvab#&M=L}tewquov_ zvaPtTEsmDnV3|_PPcInY``#qYu{N}pX{?9hmCK||Py$gpn2-L}2{!J#GI21^Y+{#g zHnnXu86ig*b-o9bXhnt+P$cBogWmwdoKONF4Mvsv`$jS!Zl=@A53b5aa5o+PYnf_A z!;X6W77Ii3$doKkuQZduQ{DFDV#|qQd9yTZTy5$c)Dg4oc{-nVQ>FDj%q2_3+vuiH zNkc-~@;Gn3h_%aPxd|_v#Pdw7J9bZ|(dkP8I_IxFPlH`&d`9sK=m7GV|7EV1Zd?hA z1T(S{*=GhG1{Ei`h1Syx^_TlK>eAH7Y?*!j^rjwmM&g@&e7at~+Kifw&VnSoA^};* z;{+N(!61q1Cxt8W&(+t6ESn3c40BS#x?EH+s32#Km}`*ta~HYTG|OgC<_Gdq8XD4B zm_)EvZ&Wl7t-GDFGgwzFRd9^Q+<2efeE9CZeE9ZauD7mi5bO`*$ycQI@K&DF7W?;p z8p>H1-qBOU|12sNEDNptUM|%Ie;f1d$(PRR6eG>Pr<8awuiTyn7R4Vj?YL!&SQS(=|~DW3Vq+r|B!BD$dtYTss1b z=>FA9U5c=1s!>?eQZYd1VorW(X8ehb#__e;jQGM%;bMof&Y)##F=J;*a4MEm;wqIa zPmayqBDcZ2V6rj~TxxREwp>0D)Q-_guxh#4;>{SCy>@?#uUcEVK1~Npm{*om6HDyQBUQa@@`RiIAwyi>Qjw%xJTQsP4u1S;b;JJ7iy2>)>BYf4r!Iv}TVI7+L@VrxvN#TTrqX>*YSeIFobEzC< z@e59mp<+);obVn>O@Ek62+-cjEe7&7p11GfeXnSVg(;&$Ix;dhRkzw%(vMMvy97f0 z9QIR%fqUh4yrQ(TV>&5Y?4IkJyRQv1mN)?+s-Us>d|yc`u*MHb#UMmeF}%#amFN&Ma$&|-!aA#5#*a$tsN8HUsEUU2vD zqR?^-D{1)mw*q)U6?9%50Ww^7JjOzRhSH`aR*DV%0q!C^lZ2ZAVc{Fia9*unknxv_}w$;B-&>z~n!>I+8PS*h*v~iW2yWbbG}>Y0}Z345(=A zTJ==vc8l})Uy1Y0s7UL!Om+ir8@Tz)8>`vW?po=5+opZWBny+mGwPj<)A-1kl2&_n z&6gd{$#44W;}9mMhz9oBPTQ?!uxv@zblmnT|CWmxtR`eps?)uD>e7hszB>L{99r=) zI;`{rD&L;7jM6AT8$_h(<~`+DcXgT{gMPMZx)n0qbq>`ADU5|7J2$r$;p)8MB3(12 zxkt1N0mUhSeQwVBixr%ui^_r>&@t&E!l6_kxs^6wjex0ORJvzRajxDQxLgmYbm5)U z7;G(wByYnC_O!%K);)$CxXi6NLT{=<4Tka3Ym@R3iR#mK;Xo$P6M8 z`J%%vRu|NhBG@K+lyVuG7019e*76(>>SWOz+zVe>Q!o940Uw<4@{<$z4m;r+D76O( zFvwm>AS#2n#9$U$4RNgVTqNXO)+-|TNC_bn& zq$`|HOikW~J-FTTeg&nbm^AEbw`5SyK4zh-k6BO|CymyTP{mVKH&U}9F(863=DYMw zb<~#&0FHI*hY%!w(8nQ#?FNFH$j;$rq-udT)%T28Rp4PjO{`d*xbO!ruuc_ESI_hp z%Z%!9KK`jbvp-ckj@U(W4lB-@uygBba{=CJx#??EhowmlTe|LXbyDr)Ww+h+?b0D4 zWa&&e)QEb3NPveAr7iq-jgLnCY!V>_uX$f?Rs+YHhX6v%#w^5?pDp<(H zwK)Rl54)q)ebEl4g9tJqQ_uIl3$ukB$R;6N%+?$E{+ipxNpm&=#4^yI?jX6HcYs{) zPL(AAdYZ?NW(RqD)H!sue*hB&VRO0IxE-F~)6kv;!K#Pu^^#e>L2m7@TO~U&}&CtjWrydaea=se90K>t#<|%|rK?_1kVjcU{%+T1;T^mk&r(Qjd&J}cJ zi=$$>-d~yd3;soRfLHScMsM=I?9Hw>lyqpFYEiDzxSJT2%fVy(3(2!3)GbHyikcMb zx9jD(y!DKlo|kWGY|&ZocafbY)w1R+Y1$_(Wyv-yw>+`jYz0o~S`V9gYnc?sSylS0 zSMRpNyE|-fD1n|TStxFb{6cGs{Fd(D)0m*K23Z}_mjb3Jf1byx#BN^F_Vqhy5ldC( z);P^Qkzkd>2kq+GbvxGorWsfJ97;BOQ&p}x`n{n^!=TW8_gMl)9T{9fRpIt+_-+vz zr3_3Vn5r6>l9+-qDD_F>F>y>8GbHwFLRulKZw53wR7D$%gh|ky5V-QZaU7N#g_Dxu zy$+sVB(N)#wDJx~g@#vQlZxI+t$6Q%&RGw4!iajF)FuDV~g*Jm|JHu&0nEU9)`3_fUaa;@NM zYOQhdUOGLcvYv|N=7>urkA@{+kA`mTgpH7e>-wp!X=6Q}C?yc_valgvPM6|7JLj+R zY1RM`<=OJTgAv#OJw4k3pAG@dKw9?*X90-UvI(4b!<4EYW&Kn#qRoin3bd%`YmNr7 zvSTYpVg}R97ac9_s}HrrlmhhTKN-?QLNbviol?Pn3c~Lc90135!^ed{7G`i$iR_5q zaQ#fhueu5|eaztV-oM9)Um2||tjsU>bR{7S9i3aZk?k{B{59hHJi0t8S;X(XzxT7C zds!pB=JNmXeR+KQls;(09d*SSb7%I5|j`R zZBzquA&7ChKcW17iUj_iNXZaBKyfe?Jla2~``&MY(R3m-?kj!pi9KivePVIOJiJOQ zx+%3^SdnR3b6Dp7r72zW^nr&If8b6#jz~LbpiX8*;So|iMdtM=M1I(}!COs1ZEAu^ zilN-akyk>Vsq!tSYwV89mN=L09vWKc_c?Rj!WcYb+y`Xi8r;=iCgc{x^zgV-5T{ey z8Wyo7j~{0x>b{|g=j;xec#R~Im}U&SwT<0H1k$ky)!~KvS&MjT(ZQsEnid#WJ*Z7{ zQk--WiG-#&M3z_&@F{<#a)3NkTB_1|)Nk{8kAj!gR8vMnTca}-0!Csatxvd}qB$I7 zspP)drJ84`kRg6Xc9c*WJ(I#K_8 zbgRK#*@~40d~N@ywo&cXXWoDJ`CmnRPmQ9UPsk$jEmlMUiEHilIm9b&{ncoKOl)i~ z=OZ_>zkcU*92aT7tJa<5va6bNWxX%zi5HXT#jpy+0q*$PI__msV0N}ZICFaql3!TT z{XEGV7&5seO93Rag=OXdPZ^$r`)N@#p0A*wk~v}FTrkf2)ix@Hw8YARVMn)C+(TgA znX34H4ycE~PK3cr+;U~wLP7d7=_QRTM!4pK6COak$PE`%J4rc=^hTzoe^ z%3i@<43#t=LjHX#8yqngKSfKvqwjf(0C*t z0xq0gJy-}0hjBj-k?vl0qLSK2WH|%6GT6rPXKz;7=k8UT{_t&zQlIZRzSY`;2u#kT zu4!U=R#PSrbHcO`FE=08(1|$(1zME)d0oKpS)~gUKW4b}$&?2+JJC18MO0{bUSERu z4tfUSi^gvM#z$8qEA1^@o9!kLqZ>SV=ah0Jy35KD2O z5_SFvG0H$dcmfjV=EX~oYJGsz{ zimu3b!Ot`!bmNIPdHLp#_Yrw?o{H76wWFDt4DnKR_1C}h{ZKJa%PM&@r7VF`_|{9y z@(U~4Cvo5_7oTI>($?U$Q4RMERr=1YT{Il;fAYBB^Lw#jSW~W@9J83z)KDCc&xD;s zJIw_RMjrY7xP%pxpP_>Di*adRX@nKObT!WGe*qAo-st0K8vuxmplJ66PMFZg1{)C` zdLx3oF!Q%NAcJdFO2MCTG)8~0$!A2hm?#@5Yc$#dX4^{Ct4}(6w^Wyt=6fZyfJ#^s z_1?T?BMj@e@I?Fkd`Kk2^U4yk3el&2{OdowqC~cPChg2^^ zb&elF7}Mn~1zHty3Gq6S;!yPB72B)}+R~<eIReM|jJ`nbJT71FDsT4<5)2dC#d zzJ|0e^uG7ggA)GK_Nl6#Qz5G><&+!fTSse2!(^?`|17j-Ome~VIMuxOHS!eqghs2} z!QKh>UgiT_b=!Zv2>G0Z&c-W+9rc#S=Em8C>`?!lp7A_fLaLNuU?17-KQ?nz+K0dg;K+3$J? zBb_et6}$F#0v*|g7&wY^@djEikz<*e8eF%C+yVxbT?D{X_HpYdGgX1kjqbg&<5YU< zq#SMDUQz1y@wt7|8IOh5i0tZ6E}+dd=FDH#;+2SXeD~gVd*V=DKpIZ^ZP;y76A)_{r zsw;8}cnsVEsRHQ;@aa>)a038xfw;yFc4kfc1{4`=1aOesw^%5{O9WFESf9g448G{q z3~ubpojU<{lnM#=r<*`$K-u)WjKnHN-SF-W8IC6PC4P*AN4LY|jmDla)TTe}&U@2l`SQ^!ff()n0m9J0B>D z?byH-kNmln9C=^;!X)WZq*wI5_%ouH2mBiO*eZY)xCOifAm$&`2N8AQuMi;&UdH(d z8z}dqaY*n6KGz=V4KFVfMcxojE(mJOhh>tJu&UXIC*qs#D;s~&hX-Gv7V?&E*g3_G zJM>|$OyUE6gw_DnTv@b8@H6P!4j|@|?C{xpy3?6CG*&4uDL!yaaWAeY3Iv&yfR0J{ zCr&KioIT+)AYJwJj ze}+51nf83Z0qHqe+)W{0K+ar^q@(_##{%Rq-7 z(nxef3fcqA@iFmB#`WmqHNbz-X6ezsf@&LC#vEvDc`04zYfP!hkJl=XkP?=-Ilz~U zqg-_kxn~e(7hgOF&|HpAyWGa3d`P0qBF8z?o3S&;AQ>)FdJaViXM^fz)eGNM6ALom zRS|P5|7hibl>RTp5o_J{JDT%4hsb8Mt^yXnMQN;yXT{g%Px#0#L&g&Q}Z9&E`Q82gp=~qIEAdKAIs|HPfwQ|NKOO6` zxfDp5>gUvBHIZ_8*ctaeuNmP!dAn21`=^wRoLQ+tdpW?q3)t;KYqddYR5_z1<25}= zKIc(sLFA-dGa*)gww2%(MaCd8%ppmL9pyQqDH#nooF!%-N+koh;3K4tbnaF|Ua_7kCGY`_~Re!0tksI1K~!HUTj zqjr9^lYwrC@XJ%$9j%1}Oyq=(enx!XX$O9QT6iu$0;WloD7k2{cc2sR(#0l zoLc6vfb&9l9v-@eDQi}S>_}?K1FTLv`&2vZjk_@Yuel17to`brb$589D#Wgny`>;z zlj>DPI}L-sw=jaioMt(dX5y8cw1T;fRZ3cxbc>Pn!xSSSU++~KR=lYJVNaw z)+)229nkA)qK@u%j&XG*`zF~kuM#dg9*<@eD$aO2!JG?d`2OG5SBlU{F^ zCh6E7)BWWR_r_m_q78BfA^D%jn5Co~(m9UaA}cyHE<=BuQ*XNp+x(8{9%IF($;RlV zT^nf~YZWiHjBrby_dXZh$Jl4zP0FznO=yK+Mn`XX2eL`BMI}u*dDn-UR;UlL@w0g5z94~DL@)*&2BxGR4$l1 z(-?4NTz#kmMtS5t9lF@v1$yhZ{)YaWcFh;Aw*l#|_XT>Zg9>w}viZ^t%!6;y_z(Vrn-<(vT=g z074Qmjq_$q-x5>&H>flWw)5eVX6+q&Q+spEp&J?&-AbQ4E8urXaJcvpmDpTWXyrj? z$q;^o*#3*g3NlWJPtbZ!-MRjHva<3M<@Om6jB-ab8WLS0I-OHnB9h56hHXW2u#PRs zCWl+JgDcZKzNqOQBTfGq*FJf2pShx=j3)@pVnfd!vfiSyq!)K@KY2lNS0QCYuO&`gCa6ZfOE;_X67Bk;hI^6{;W;=vD z&pRLrw9jRCETyAW|GG=oAh+rL2AXFSwrPJibLOnO7;>@G{3_bP+9K0>#pd{O|GfAW z(9&LtbQbSRrSL~E7y^+{_%Fh5@akRu7U( zwIbf)tA3Ui?K!_e?M2+pKAjpeoXw)IETc{fI|plcw69eZL^1Obq?Bn5z<=vHEs=BcyYEBMhY1TyZLFUlc-rDCKxVko8niDu2nRe>Epb7Vz0~W_$*@@jz!amtjnV= zN6UHV$&EG}_g;}}l!ajQVLqsH>ktnT|6 z7z&N=0D+d12Mf7x33iT}8Jv{3I5X2vNFZcoC#&5=%2Q)|)>Ro2Rfd4z^} zKgM7{yM%ifZ1rNa@oSqbP+GgWH+5|s(L3|DaF4#a@>tI*8@i8$@3ry-y!rKqw;uHR zZYv``1G>l}4Hx6Wy`Xx=OG5~fFOwa#Y98|2n~GVRY-dh6FMgL+v)w{ZtgJ*aeKQB5 ztq&}`t)yzL4S!JEM&SVPTn?ssZ9EOH-A2ju{K>TKXjz5{(I%e z$kd)qS1S#Ylan6P*%~fqGrMN);*&1ay(Ya?7G17O?^zOVCuSb_e?!PE5GMDmZ}) zbUN&kex)?PM12IQAaFXIk$yEYKQA<(LVQ>U{L!#$e4{}!G;j_?LSlYyW}xj zY;1ytFd~8282->zdj4c#L118}(;;(yfzSCkLH-aXa(zmn3Xr~ox_6{s0KJ2@ca&cW zt{TUu9kxYZL;ad}gkOJl2c_EpcKciJpuM1V`%~{gyk_2c7!5QaatE9V|AiDBBfhPL zos)Y3uSTT-41|hI@$Cb-;qEXRfP$+=qt)KgbOG5mK55>OM0WQZsO~wtMGxy3?-;iM z*Z$MMBXo^*p{jkjyT!o%)7=lg=dc)jK%%DKA(46{$bJJyYWqFjv2lT(?#a6a72}$m zW2E?m70>>w|8%&FKfrQU3k1bJk!0?Fif^}MH^HlQ9JYT&iWMRl;@D<4XIBF87s@oF zZr3gb?gd>Ywk{PP8H(I=3@7C3q+2^`YrGOWY71sFtq!>bZN^ETagnuOWUWtBV|L6r ziYl-h6M9rmzj%KPQRQyOL4?L|OhLBT zZINZndGNwfJ5s`#o17b8J8nia#tB`aS?QlZfwUNL!JeE9J26H=v}kYv|2)r$Z-e$a z@kIhzp}mRJpu#e=Nqd9DI=MwGvsijT#5}M`M+22-S(&6dc}1+U7{fSxp6zM1buA2L zTU;29?kb;i{6CF2NRe z92$zlhE&ZJ*h~(t)Bu|Tace=9T)LjFLivp+U3Z!&E_341PPu-AFiP5vudNh)&)7yM{tdZ59)tcj+% zYLws9-uRp!B~Kwv!XDZl(jLkkRN4tT@z_3U8s#l6)pFZ4#}S*&Q>qoz&?y#@Uc?&* zdf_PQ30esX1tKX-h$tz*k~Jwyx<`~ND4>zebq1pK><0d}IV`+vvXNP(LetOZ8Yw>7 zy~UL<+>*IvsOOnaln#0Q0N+hM@~V~=v~C5@IpaGD-q|)TW1l98HERN)nrHDUw(+yI zAOSwqQ@hKyGCen{G@rrJvIb7fHR!oVI38)x0D#mJh72u1}SYUX9HIdsfyd zwk0m+B@BW+6Hf0Ei(86K<^ALvMz2EF3U&o7{oFd3r#O|!ZKG!I{q!4Vt~DvBQ_Bup z>ltcZgIqa;WqX^@CD#9LBwkcYM-nW;>QR`!wy69-C}TxsrM&ci9gDWfVQ zD~-$C@`VtEl7-X3FAAOQh1-U`R=p;$jO&zFCYtHhTPo&?i&h{lm@OKbYQ@s!O*ySP z4IU#E<|}}gR+ScwAu(mqTq>@lwbKU;PIYD89BLmL3se^A!uM)h+1)sjl`T6x^5*g& z@)qt%2jLcrJD!9EECW~JajZ5$t09?|gUE>$OsYtU3u1DZ6EwLhSv0jCGBD{<&r%T$ zH5`-DVeF-)nt~A708c%RTQEUA~dzC zHR^^EXSoW=ap~4&$jIQ|jtUhLg0tK?9jDFtrzGeJiAlI~K>h_qdp12cyJoZvJl-6$Q*;eucGg@ynVkfrRMxUyns-D z=8U;e{x?LM>HmUgGtkj7viyHUo1Ko1?SDtK+x*BH$qM?~RfUj-07$Xq<>i&+vi`=& zSK+bxg-IaE)7uNthb#Ugw51gkr7e*X*+$aL3g(-n{}I|%5G}kN1>(%=EiiN5wM$UE z8_T)dmf4OkKQla^+=H(?y8r-p{K&xO<>%d>$TZ~YwgkDE*7`E?DUJIk>jHiu06Mv+ zNvZ0iFFM@dZlQlzcD+su3IcsGz0vL>0XKlDDYW8w8&vI8@rVHaDZAoQlQ+>kZ9wDf zhpJfX`hdR@fGnrqXic!x<&;$5n;m|5@EW1v>U!$fNFhsi%|Np>r{B^Gsvt^lk>&F^ z)}~rhOpuSipQlb_r``M{ch$8tR61y`AoUK>R^3g-a~N-Dg2fnT0KRbfmqeQD`X^gP zQEYPvzV78~@Q6BRxWvj@y3ds!CyQkPe-M zB9C8JAk&?oZl~5AUwv?GCv-i=d{;3Umy-TRH>!|K@{j?-g~2)TBXtm>bkhS5(gBL2 zMIucNU`+{wI>`GeBEiESS+y=M5`~4L3zioSS>3 zuq>if6n8(7WEVR+#qX5H7HOnRaJbKG`C>K+iUtv)o0Aa`<72uf{B>8PQ%B-=xsXUt z;)f*9RGGujkXBZbw)~ejRtoAN*)l2lkb#wg-5}wzV8f30{NIdrX-o)i$t4kX=^?Xv z%o$U5)gp5?>m|~EGujmIa?w)pNZEPi4;?Y{OyqIH45MDeiO^!hQXV1`MOq9*iYF6X z(o5)1_$ro7SP5F=g=`qK%?jk}359}t;b3lkF?3JDyicOIQX@!@L5X*WRQ(1MrJf}5 zUTkrM?S!vKU@^s^a@GD$IeAX(2Y2|2pJRGtS z|8R|H4Py2+e{s|}(14=N!1S^5FS+`cC6KuzO)t$a&6;W9H}V-RnhBa|=zNHQczuvD zpSHkOKi}zHq>W-(^|Iu{FnrV%dLNyd3Fs37q#ptdKO*R@ z!M(I@r=PGXS}wL2pB6K0!-|YPI#sg$A4Xe0Y~Okd%gRl^Jv?4NZJ$6SO+RE(Q8@%{ zIYW#&1cOv-D^Id%+-*IxYc6G@FLeVy3~(+7aNd7|FfCv%oP#i_qmMuSOkx{OT>eaC zTR$FXo(Y-T48uc#!@!KgW1eeh-fKt^1yl|ORDlLm5eHPB2f++JWB~!Hlt3dQ{%DAd zf}empm|iJlUMa%92@BDTctjDTdO)aRNJ%5qyb0&Lsb3g>s<2v(Z<9W^1wYY65C2+- z;Zruhtq+sas}YF`=HTY49RWNj@skUeA5fs7L>|9~yT^Im6X*1J9D zuLB_LVR+Wa@>b}lJ=g0g09V-GM>w2`e`q%80~yY=p&M1~;eFN=q8ozj33=AUAbYT+ zJBX?S@~S;}sy#dHDTK~=<(s+A;H4Y4?HD%fTj_yh7`>9u$Z-4DQ?YpX1Q0xPyR~F_Q zwNIw(Jvz_i>6>)@!_IeGnpq=%&LIW3S;9#jf2I)R{Z8dNHL0$E{EVbGWvcw9UJ={^E}ijagWy5SfC z$+r=lFNwI9hR2<7_^B!}+~gV8#JRT8oTr*FZkwfia3w-|#9@Uvd&$RgMu)2|&X}P4 zKa|v9OlpFn;>onRJk+BiwME|6y7GES)O%U>o0$)YSuAEU5~I+&DfMO94Li@CL(bFE z7rQ91Ido=eokQ{zZNvD2E8DWIwNGglUZK1k>9E3!DNW(etjFMS2QDA`G+Wcs7o|!* zsHS`<;~QZ^{mj$L-XEBKWvpR#tpU^D0TA>i}@rzr=86lH?#783RGC zwF-bPX5wJ}3`u{EMfEs{jzrP*K!5WDe~X4rwmK=-ABw*C8eLzrZn9l#r2?h39yp_uQQ2-Oc+?*8c2X(1bUEdDdvLKFuK?%}-EJf%i!bSs^d1?$9%c8&>5H(Soeu zb*_JHpDOzYFc*mK!Iq;P%CT(n8N+(R&+4A~(gz1uYzD2~YnEp5BvWo+>OS&Sx$b~$ z{U!~*GssDN51_CAzfbnoIxcgjYRdJtDQ?3V1FV!Q?4TbHkI$ zNflGztX+{(2S+32`RT6m1E3e;D(l_MMO_A^-0lEtR`qS4>%OO$`m1pdpEs`0AKC+| zu|Y{Y03Tr9kamJ!!K0MpQ+9>9c1|C7pMlOjgn$1@f7!K;*NBTwtj~!x9pV>N_mr~) zYWx4~;x9nYQ{^6=z+D61E*Rgw@@LaFbJscN9e$y(zT16gQM8|Mb_omP#KF`80A1Q0Wc1SOHueJJN3aa~=;Lva-UD+oM6=OBe*4y9Q; z=58g%kc`AiCc_kysxtDoXPPq$n}tN7(p3bZT*Q}R=px=syb>8yw(iKq7&_CgG2lS6 zd{WC21P){dkA#vKspEun;E&19WwvnHvT5l!)ornk_yTAn4q7HB$SgR_glu}@gNka46H!5S7B{?3P;@!eH6y5g-7XB$I{a;x{PqQk#V;dKmKAWjN>9B z8dk|k!lrm4vqM7LmA!aUGemJB^Ps#&|Na9|KQ=0~_l5h)6jv?B-pha7m4baJtX6LbWl zVhNM0%P}SBZ1m;KOuEl6i2Y!PY>EA;vo$176H*7F___xct2s5maJ3a4ONL(jh>vwB zoC7*Z!Hqn52I)$%+G0Py84EkNu|1L&^y&!3v8Z(wrVOdNwkIQ+f6j|WJz^Vap)19j z@6#PG3Y0%~tm;@yaH9o5iR^}62`eT|_E)pw+j9X-CI%l;22yEN5<{|>w)-ypJ%p_T zjnxY@WQ~cF_`cv}>&<(TMBz~|_&KjO5(HJ(Q zLISEubOBSu+2j$N&p;MT3MwD8e2i(0WtMk z2#2>Xou;IOMDH3qkE7wa_5OeY}UC571CEhqr`2_;t85cS%|EajOZt}Hs%W8o)0x>@?`6`$S+MP z$4P%}52^A)PdA97kAMn>1e=W=n3N%=apqk(snymO9v;b84N!c^qparV6Blf6<)h684FNXbRyBYhvZ1Y@@t7i88tf#15}JQH zGL8AycBd_5)>7bUWZ)U(H~SJ285?{4>>pO;UYAu%kxMp7gIaD0zqz@&p`pp@9I46D z8dfwDO4()X*t)hu-E7k#dQ_1@A}XJrzkvC>PJ7*ch=5xy&C}GWQ?=}+j8mG&X<3AbdY77mvXZhvirD zzueDJ&D&?t0h{EqqMDK(9b~{~A}o!t_W0zLN*(+?vp=zLaa=hO&wTPZuRI&GRcOD!=vXm8z4$D!e>&m+;gmU%%TnTY4dY zRaDdm-3oFK;%a$znw@&fr5YpxbMmy}$6bX;LQ>vDBn9ImLuDCnNO`RI%&9L3g9yQeD z5yYb{RtUAyhh0}iDc3_K^uS}4(t;x{fYE{wMU3DeS|%GLiX?oB8U6|yI4Jf=X$oXA zCU5cNizSOcdDnC43%t%^o99rp>vedCJHBQZrGp;?4*|!ch;b{$aOficp zlScPod{9__{Oky6KKQVh0{^BKxB?K>rF5-R1puy8JpPP?5hcv4kKZDw)-(x2z-2l- zGo#?euUZ3%7?8!o4D~Ca+}p80DyA-^Q#Q6(S+v*}YqolYB+1QH5Z&}|Ul ziBy1rP2|~vv8F|_3RhANRM+gREY#(XUyj4XNXI1Qs3r_f z_87aC^17VH^X@#wZkff3tf}7a?ocm6V{2xrTz5A6O3OvVjT-nU_qum(Ai+x#N_)2= z|1SI{D_B;zvhp!nc->&wr#<J(--MTZwj)@ubh@Z`@(~9WSA6w*5(F+#X>W? zH{*77vc*+t0~lIsmYjW~tvD0CtuCa?o0rUIv-BrWq$FAT}sEtj{#iQ9Uvl_PYKxW4$Ag_3%L=C9@#<+L2pKpaU<^gBq<1h%7UI6K3ij($l= zdG~t!oIf{!UawdUHvo7@bt&goTKQzb?&u?N)?xfwtsYz8uzonOz;1~gM$q-d4U1a@WyX_yXmRpx+Stnc^%VTT z^r4AO>G@UZ{o(!li=!^Jkwz|Cymco+n@yW0magoFkEYnR}1$y zBiE!kpVLCGLoqbLBsYaZ>*Q(!<6{-^o-#SuXdLz2$ItzpR$RoC5qjaE5?OeeQu%eC zBc7$BuiqcVqoW;J`Jm$f6kbF>;aPkZ><=AC25Wd8_(k0vs;y-4 zN^y4+CVe`iZ8`SOD5e~G7t^Z?=z(c+>QnPysGe2&fEHU?dgjA`a%~*F9z?#E{S zHhpjz6*e;23AT|}faY*#`bJ`jPD-5m?+U=2)?iq273K)qr^l=328%d3)F|Gt%!Qp>i5v_G6esimyn^G89nNxkJ4D* zMFN3uWIA@3698v;cQkVA9JGcv+z(DQCGMt}RZ1PDjg2YZ&2f0Dc-7=ZBoViv%CHCm zB=hd$zIaSC!bw2V*&9V3X^N6zQ)PYBis4G{jN>=?rlyX-`xtH*3_(iw< za^M+IWc{*%mGfI3G5o39^zP0WTcGz$e}}(?=cGEE?vrVxDFuk5wus9w1jQ;f;kl&o z=`GK%FM@XSTe6@S69y!HvMv`DWpOGI`{EYYE~OA)>m*IfBC<>5RBu)^!^p>*Vu-+a zLoR83rPD~WFnA}{RPS+7kUa%&c-p?IQExR&jicKY)1h>5U3v8GW}0U~?btNzQmzGm zVs2Rk?`n=i8yCDAbRSk;0%Fubv<3NJ-Go zhr_NS7jK`^IxB*&`^+CA5PqLPUe7g<7M){z!1nh z)icGOgVUQfU{bX!$t=&|+q^oinC9Rjs=0bhxkd@* z6PM7<1w^x#;XQMC)fDc@T*id6R|-K9Emz5t$K&?{roKwoZxJ8B&kzk+YQ^~~dy@KE zE;+;n^}lsG)%E7trf*wpr*<@b?0x$NH+p@3)6;(S{a99qYkH$DQMToc#|I||LoSI< z5-CiQ6~fyqP0EO+=9J9YE6R&4E|*P8)yXQ0B=546$`?&#o4Mwb1z#G}*Rvf()>+=T zXq))i7t(>aeeAogL!Vx^k36%CP6lN2#3Xhggzr=)p?v!G7*RJbhmdjy(x9bc`oDRE zDoae>`Nkj=P_W922(h zS2+WPi#0EPO&oCO8UBq)ZM^}#P;E*=DY!zf-a>r`Vld(c=IN}beVFD*2|qZVzn)|< za$$dkjg-OinUF^ImBJu|+2M|Swiz+B1-@5}JhiieZAuLohViESm&t&A1Q$PkJ+1I?UZP|>7 zM;s?-jJ0%YcRv#MwmKR)wJol?Up1$X3E4^&g|nKQX|gviZ>3?vgmfgtt93Mq;&|%g z*S2y_lPEA`K6Mq&?_chq+uq2u^liT9m%^Gbg@DRK&hCxojm8GzxZ*#1ln9w^lqp)6 zYZITYv5I-vqL+=6;tn(8_VavKd}6MfZY$fkxgD@YapNKD^n@!Xz&IKDYIheH_{E?T7zW1t9xOkXcd0JPc{Iw;s{XtZqz#(vO$Ur=ZU3?W!(;CecqZaI|IH*%4&^9 zZF+G6ax2zi`cLZ^zj)!@VflXR)^=y47lMdmNz+0L@l$ncf8K3g>YQ#*WLb9^ z%Bx5!U8dC>!30e1vgEhxhOQEu^1DP;JBD8=5kK)LZ%V=NAS#JVPGor6EN&efSs*#Sq7{KZ45g937kL?zg)rtc|KZ4d<>N_< zl9E%Z1Au5jsjX&NKI`HVdn?<0qJXN=Ti?8)pDxg!b^9+MB7d*i8cb>wAwNEi@9N)& zP2{qGcPcF>uM<%z48)N=lI>H6Lk!SY8$M;Qnxj6{JAe*5cI=Qn^`FgZOM@N~vb;?^ zvAq4f8bP5|rQp6%^5hi7^@3s-cU|A1k=v|^+ljwSO$l-jBU5uuPolg_qb<0d4WqhK zq*X{g+D{zBm7^(WoD9Bwo_Z_uM{<>HrJE)XYR$F1Dcx;vPx|SpStj<;#X=hNp+SCF ztNR3uGbdS)TXG!=a5#-S$OGQpdj$z1V#-G~#cMjVbZdA7Ncv%tTq@`EV6oyCJg zp5}mftY9DK6lu29VkJ23u7LGO;&>CpQpA+ofTn}a9IkHQhsN9oB)*9fAXWi=!#=oV z4!-f$ftgmKYJY5OK@yq{KKhIpmD%62uhepM%2nhp+sP3cLj`JS!Qv$}NB^>XiB zS*4{fOK9X6QmHg1Fq*Odo*^E8L8j(x?4qAxhz~nX1I4zaXEK6$@OE~Di6WE6(;u~S zyo9s#|KNCFmIe9kq-=ku^R)L0-lP`!vA*>9fb8I_op+62pv2 z7|U6=_gE{Hb0`u3{$2-x3;oFC7l~`VOq(9nWK6#rt6NTJHfrT}@LVSuTFBt13L;px z#T_33kM&|zc%u9zj|lPS%;6uDRGVacx__Yqyv+F7L>%Hd2$%ibit5;_J{&RFB~QvxJ9C(u#pCEq zz_E_jW)ep|>kmXy=P&yYW!|;>k<_+(o2FC_S0tUy%Gik}c<@V27EOmV)PXs^wRCq2 zfUvWi3NV_80GPWQx>~nITGF)UPV4@Uo{H3xP>0D8?t(p>vlzq^TVW`nl3X5SUmXkZ zb$GCCN|1ikJbN^W9eQPWyS5tYW6pUnjJxTElp;w+u)y@fTCX^5;~Gm1CDd$>Bj&#x zV^?`3avKEl1dXeC8-)a8(R!-d`+&Y`LQR#8B|B7BSo(mb;OG$=2w)$xeGGn7*YIr? z|HlQsbHRBgm%R^a^ZC3R1TcOeq~^@9n{@n80`gK9dAI>a7K zmh-h>>}6x`cJl<^mF3h~v&1e#H&+@46qQn>I6+>b#W3V^ftZ$t&XwcV_rAZ_3T`vE zkLzFBLPO>r$k|^YukIF_D_l?v|M@QAYZ{Hl9tZ$n#09p8>wSqCm ztE| z&#MvTu3)`25XB-yGlZrhGb%QrOx-Mge?7op)HP5v)a>CZwo{n?~Tl~9Ph9tYe zIXZ5+Rm6}aQ6yG}0BH?*!Sme972Md5!1t*!2;WwmNH%~N0*O!XGMingPpEfQY{)9}~rT>;X)^Zl-JAr5N+>kmx1P`VhvtGxtr2e!^)I1_X! zJ1zgGFUM{YW(=m8H=5PEo!=;kc{(MteBQXSLG%xiI`oW920ewO*GAOLo?pGiqG+W#FwN7mQ}*{o3Rpv?nl2e!%siZr57u&{3OJ*HDqV{8 zD!)!DP*0u2CXQk=5U_3=U(@d`eNyJw5*aX_5J~4Nz)Jgszs- z921i;?kBJJunB%4gpi(Af>1 zWJDBA)#Ts-jkI1{7dCj+SJg&Xu<=*t?q^Zs+@qJAs$%~PrA}Qr-wt!v=0WFjMhRAM z1B&!6GiPC7-1e69Za`pbyH~Mu1Npa}n^O~ui%IbUuJ~%@V2=M8;#2Fm*s=5#RRMC= zrnbxJrScQ(**kn9A2C<7?$fmr^$xSHmCYyzbu(L@ZkH78 zA*5>%j}y+>GAD@Y={OKLyB)Q93{=z)Zrj+t#i`Ytaif2wN~)oOobqQP{t15A5d=!wbT=GG zF-6bAo~qZ836n{G_SyGwU==lVm~TLNyW|r6-t~@N4wV;5271(yQ+9@=<%lbA&yYqo z>Xpmq=^W9*1vsW+P>KM;p+340Nt`%&8gRBa0c;eX+!4ZpSb52zh;kgC4V^MGSt+Q_ z^cI8?$QAihY8r|qm|uM|UQQsI3|vT9$uVU{^e3u>9Y*_^?oll=X)SlT2QUf+0vmwQ zu!y;n&E06tR#r8}$I!J?o9YH*`VA`4FhjjcXOX<%Fe4LO+B%~6L^cKTbS9UZr^G`D zQT{RPJ@`--_nOPgZX4eK7hqn;s6Lt$wNb6B;P1Uhkg9yBfl31MykWRICpo)P3MOM! ziTHW)NR%%$JdT5>C+G)LbLw7Ze=7@Jl=92EK<5c5bz`+oX$=p*yQ8sZDe+YzIYh76 zSdX7(5xd78qv>gdB&P7Gr}knooOmNraRsBNFY@OfFeyA$4e&0Qf-X>e`cU-bAf`XG zL{<~zH}|cE01Sc@L^2Mxl*xVFXb7yM^Z>D?yNX-Spqib*eCWg~tb-t54NG4CLJ|ry zP?tR-zIbz$1;ft4_x-9i<9*4@a z)eDbM&?5W9EQp*ww{MR0=yGI-BpoNl9<(Ijh;tOhX?K6&JQLdjq6l>@E1PS2ZiMjZ z2kHi|?EIyP7(m+b2-D-KgZKGMKD}AsuQm-jDuFRCK)_TycPNtka=MBIkR_eLwbbbc zV<%QWz+^9z(MHHCcEaZhDa1SGo8?mBAee)?LAnL@X|ANB4%`SmPsIZyjJD`&hv=xl zkB5N|%-i48=A+|@evVY#{1K``7QvHR&3%G}5M8CPd~%v46M`!pUb2c=ryZzl8Q#2M zGFTpx3SCQN??3@Xo_0Yi>k09;OF&Za)ckv>0Y(w&$bW-+W1t9To9vb67`p9xcqT{KI#W3 z6&kP@+9sdz2r7g@I=h=Kd{ix!TE4G4(gVzuf#$+R3!_mQH}b_UoC9Sxqz@5PaAbK;f!GKoBm3Kcf z0!Y<`YUzE7e=*Q9QiIh)T1w)%2ZcD2TIK^IY7U<3sbAGb@ds1VS=xlGKP2jYpX9-y z^55?IQFMSgEsmjXk_w`calqq1Qh*|bgTaWI-6e3=7>cxQ`voHx5{yp5^FJjIn@gN3 zRjjILfIwCdr#_kEaow3Dmn&CXdPJKtZK(3ym&GUsl8Yln6RZ9RdL)mCIBV~F-fEsw zKb}}Wn%rOIpx-j$aFQSb;gC|XfI{iy!S%^x1C#a}L<%Zr6e)oO$OW50nESFRBX1;q znHx2x@g_9;T5By;RET7_-e$AD=6q)V^E$>( zc_<-NmX9u;+3+;MkjS<2pvmTR3qywL_6jP@!Ks!@1|zxmt&tUR%;-5o4VVTW-qUo|6Xv41+M(>7}6 zTfmhv6Z+aj7hoIf><^pRw{Lj3_iu(L$XMvjL3KHG@Jeq<3{NyT96=B$7lf6!c zlA)3xqDDH@q}6&qH;i$M+q;1hNRrVGsbXkJf~8pq6zGd^2H{0~C`M<8#yt}BI(RIp zX}f$3m}gY=a=>O}ze1wA>&*z&xOrcCG(Z1&^>wXTaHN{tS1V_k4_9l)Vc!p&H0%KJ zDRdH840`sohL9CQ(03odrtqnkjRjV`%a(&>c$5HE2UgQ7h#p2$%H|gwV=3t z*2plk$XaDU(^2upd1$I=?@B3#z$m+|U-#XUEn^fDHBa*X>(Rh1#s`WnsGrpLo%r)S z#e|cl)L}t!w&Xg+Y|M8bL69>UWo+6qBYhuQg;Nn5q{wliRt+Iy4&<9TKlmIkCSj^h zt%Mp=!Ym+M2+c1J!6IZBJ=0jPc1Z>1KjPNZwRaZftY?b-LG6?fZ>e_-K!lGCE8}p9 ziV14MY?>&!!LKz67Ez0@qilT_Ri@TlE;q2(b1Jhuy`P;d(Lc~;P=3YhE`@Mq&mij}lCMLNPB!~giP8e} z0OD0uecxPW#rUNTis^_*(B)<+m^O|}9l%r_HNNaSN4HH9^u_`4kB**5I5bhLx(;w~ zynGcQR%dYzN(?!8eNU3pHhv9qMH-~#7dw4?GJp{vDy|{mQM})0FPTk!$ zq?8nU$5|7DMvg9LkUog$YJgewnTDNWo)n&6Y!>e4jT{hZrp;KSe(!~%I8h0xETG+}f5mS0F} zcAm{`n18JlXPj&~cnE`qq%M9_l`d%DMnR6>TyF+EaZtzY%&qQre;r<1*3Yp!Y2jJ2p9(}e9E!jPIh|wPcwC-PQ}Pya3|rX95dZ_MbCsq@0(NW-4xe)K0pRfUu004ifYYw(B1mDaIK5MxwRq!V<)PAJ^}^{jJ+bsj;ER(lbi zCKMSA`3@1KK<=b|!Q|@Gz!?n|8ye#7I0@;2cYPpG;v@@x*=$p^PZj-0^M-%*`^z#l zGe<)bLO>Q9qv}^_R`ar+i>w8){>MTxe@>GqvY^qo;}7KghQ67I}a#a{{bZk)221(M%)+&xgY_N4(k}VQbh}^jg)F5 zv(&R%_p;XF?`QAM80&I>@z0SIvDZDj&AIyiC)p5@%}@&q6_dgex-QRmIgai4=yV#3 zg8kuCUG7f>8+X;ovGZ)Ybhl$SNg~um4N;j}kNe8z^NdrOES?_LeW=CIe-ACQ;UD;A z$=^}I8FQ^E1xX-DUN92sZF<@AwW845d;s+{D2S5@$Tl1b1-T9gC6#YwMQp@DWCcb$ve?op1eG>wDPUzNa|>zl5B>qfi%lFNki+~{RmB!{xIvNVQRL{sgu z6wyD(R){@=4uBd+=#q{Y??a+LS(!lGh}$cec{$=xLBIiLi5(11|4l8Sx@R!r;W4B# z(ObmK6`$lQmhVjA!4U*PissO&wH!^Jma3h{nkmsEmi$H!97ev`h?R#wEj{}m8`Ofb zxID~uo40wHY1#QTKpmU&v?&Fhg#kA(q*W#3D*;iUmhz_50?pY;3oO#0B&_5M%3C2_ zX3#R)8t1{XbiC2$0dtINU#Uh3m*=npP8EIAqWPc@EV>DGam@UaDSoYb3HC|bfG;!o zzqdP)1>IV(ipc8=37YB@0^#hrO-cU_dNu_ARu+0mS?%ZM$LK7~am%v{C{p^|9^+23 zdhchv|6t}qehleK&Qlq&XvF8Lx3#9tBh8&w=MjQgDnVE<_FvT}oQ*V_E8e<`{cKo} zJE6zK!9Z;Baj0^d-cV+5e#ICf!s<-X49k_KAm!}G3>|5+D{O1s&D64L6=WW)SO^1# zec2iu_4<>$JBwhuY0FlDkZ1l?XKV{C#EJaZ6pwo3t~BSaH3+{GDDJ}5#~O}Q$=XSy zg6ViMybS+lC#vv^y3Y04r2A4%gQwXjm<{ipxO^h=RDbU_{SV%5IL`8hfu)2fP zAgNl8QFOxqAh{VV%*#`UhE()y6ICVst%&QNfY_4v7X=vt zN8znJF%X9lWnmNt3}qph-fTLT(>YySs`C2eEH=;d`FpIz`d2p&S2n9}+);``<%$wY zcih7AFl+$o2f}`-jssVyVK6>QI*897?sWIbvkm%gXYug%Q?-mG?Yl77p$+(OvmZi zI&RcG>n7(PGZg^YN4)?2IimIYmb%r6bI1gO8nGM zR!gw$~mC(+@T}?}AoN;UPpJ{keyrM~9 z{4M*M;6CSm9z{{WJmT2S4}wC&sQt-x-ie7#d?x0_{biM$-3ok}Wf(;=N2LG7*gL@Z z5;pIGb7sz1XJ*dWwr$(CZQHhO+qU%^+qP}*`EPRX-raoJd`VZ*)!nI1DwXbjs_T88 zEaa<;7pl&}UT)51bu6{F(|9pKTbYRLaFN6%qv6W5_!y~yEaZn~SN(7MDpyhTT6(PC)3pC1Orhj0hKQ#LMYn5&R9LGW1gM zZEHSKWLyfo%vL$2LPC(?9mzZ2-ouae*i}9Eytz2gSd37GuKo6^6rV(igB(pjD^ju+ zD${@IL%NxpP&@yG25e3C*sTpsq+E6l1l8^+(}W~U=_6|*UMe&&GS5Xs_m%%n=MFkg zm2Ljpqps@|rPK%aIXiwpm5x{jmkE%-sokL|NaafbHg+_YW2CH zjHo1ZI>O`>fb8qbWlrCdy9O&fs@X1=v5#!E-4?I1pwe6t2<9lZ#eX7DD|KY7)&+n}28=9yZ>JrPA6?*1}M`<8vDh%zvDZiT3ZPsaUl0(pNM-csXCFUiegQ zt+g0e&tT%d)b1!J<}P>O7j*NsY^lp{>!duLnzYs7v3w3uj=VRqz97?Coa0*1Mp29N z_0*MRh;qu?G^DM7rqswfoRM{%wxH^?lQFrW!MSt^3g$ZI$y=?Cq543~ZYcJK_2aa_ zKAc0*R&Q44^=h*qf1s!ai6pm!mtd9_#8__~cqE7_C2A=43L5mKy4=Kngccj*=reDCa5CPo0CLlj& znKAyDLH@aqq~C*|-VB7l4#5ZYlE@0aT)hpWALd_wEVI^K(V zF>=FPc0_}ccd7R9;Lt}8$ZxXv%L+5dmjkaicO?dJIQOy6v$812aftE{I>SC~Iq%BI ziC=wsS3MBvA+)QkldOz%(X2#-8n-qfUnQEY2;F6qIrTQ-N8}3Se~9MpKuDzx^4eTh z8ybfH%+d{wH;b^Yn>%?jeMWgPV7z)$fc{0CPLyS%rSt+__lK?m)t`m+sp1;+jXcp# zBGihn6y2S{o5tX=#-cD$T)K;a&{YJ%B*HgiZf zq&T!Hfp8|92-53_ZMIS!&>&;kCo#)8-5Yb0Cmmw=sK(?~_zB(m(77Sa# z8dum2p1aSNFLD+4jQIK2l_{)~m^H67Ekx-d?VL}Jx6nuMk&|4TWY6UD?63SxxG;W> zJ|3`>yUWm~UtL66g1u2KP`;%HuN&mgf4awbOI=q<&Q0JK46RU}8f5_0|1eRp1^hqC z1RaA1+voCLdvZDNz>oe4vK^jTO2DZg>TTdI=u$5*)xiSYcIqv2fO5qT(CVb)9K7FB zq2Z$=^t*ju&AShzjuGeWw~ig(lQx*I;VjxE6I+0(tUvTDR>eD77W8;<<1GG3KPSek zt^m{}j=K8~?WNBfZ=Gte?^tf#y?l3r=p0@b$dfbXd0GX2$c}tNYRp=y21|s)p6(%rUy397%dzx)CM`Rp3I_R_-XHDDzu%_68dN_ z_sh2S0HFxXOpqNY1sYyBG!?`if`OkEAR{O!RB0f=DgioktF9B$zuem0%$^D^P=Xpg zB9G2ka685iLPa83US+ETb<-|$bpB?s>EXSFx zhpQKFSKfOMQ|j8gh-4(xNG$*lDtkz7Tzl0WtBDo!gUSJdGx$`pd*z++VTk}EZuXvM z#(m5^;W?2bByQny0ks3b>bpE0HD$rU*!`e!mJN0%DePihs$wuhdkMSi4?Et@q0Z}@ znGSq!jq(DW4P!CXsy;?c8|QXW3P~CIp{dM0J*h7$mHzZdtgw{4jzHD@ZVoTMjDFd| zUkn#&l2>uX&U;pRR`8SD(#L@pC7~r0u&}uiyzNdv^hwQJ%YJKfND1_6;jy}l5>2M* zl>vsfL54L^UE?n=aIf3E=!eK-_v3l02d+vxumFs#IX;`FGHmE8{JjhC% z$1sf>jqWGZ?|?4~Hu4LR(0Q%V^4u{;lx7RXtKt{|IMmC6l_t_7b<6NF+giUr-3Glv z!7)UuxU(XjvWv|~eOwX>-AYGc{Tk43CuNSg^0adzi%~}JBccG`0-1z49><%?g04kXMiJa3t7Y_<$<~sRXq9MnsBTF{ z1V%&QBkGgALnYYsF-3Lb3u!Ai^p$}|0rlwibYNrIAWy#gvr<)XnxCcQ5~4=P170WW zi}QXCL9FS1#%=YEezn!29gw|kmz;ADx+`hRet^YC69;T%fGs%U1BH6V4C1Za2z8t zbCy(6ihSwwep!or-Pi|ajH??(5+J#O-;JyP7D3{uh%Z$o^iB~lr3t_Cw*-u@Liwaf z=K*E7esJ zVK^j&Y#x?@12AKp6c4U8PQ!q1a#5R4PG8m^9?^1A>t*{GAul#z-dXw zsWL}pO#VqGrJa0&z)P_k+Ih;)$O84VDjHaqOC|SvpAR^!l?lEI=!>8C(w67BRpL6f z1CMvBB&!H+`5Q&^12g2~gyjbQrmB7s>9PK~RrosgTIITkyG3>nbHf~TLqt+ulFC%` zRR48c|L`bv4SkWkwC}^|k4IcLKkeG{9OR!NEzIMQ#;Z@Xjgi>8);IZ?B7?l~=%-efAV{I0889zZ2dx%TBtmxek|76v7YKrmOmk&|pB$@G#r@jp#rgK_sBL z)nj5ty!fH_1~Uek@%dj@sH_t3k)44b4Z!sBN5* zT%Gd6;0R;h+OPwtd;uvd7|#cqqHB==FQlgC>iXVo7Oc{61Dv*;m>K4Uu{T`1=Mg9V zCHD>KunSkq&1Tey8d|$3Go#bYsj`V3 zE)i11XmpvR7*~M(LiorH(YPIw^rm!Ddj?r0>0D8UOOMEEm=bNbFx)yE+ORyu6mdkm z-a{NkjRIA-pKS1uZuuLz6}YzWgvkAjd8XkSlyhTsixu*-$wbTGw&fa}2fziG(ApwO zEm6AJ3X{0;DrJf+>);Ak!R#gVM4TVLMrwqQ(@>V?}K0WPZG|l~#k#2x) z^eqoz5|Pk3YBryzim*mxQionMl=@mTLWP(~2znN^i{)fQb;9OMeJBR_Si^vo13BL~ z&GXMoF4%-+%fK-=$faJmv zN(}t-+yfKzobeDDy!Mi6hf=wcK<>#SP3CuL-InFXS!TvM5sy1$pHbUrQ)q zHBVuYo+u~KgJh#)w9qoi(1$d;67kH7Yyt*_iV#~5qwS^Vu^$}4Cos{oM4Yn z3yP7)8_|)^2`lHN2|39#lM(1E031Pyh796tOI9{1QT$!-0KjYDb_&oQ2R68C=y@>gwQcZVK@ zTXfg>KINe?iY@_d0xICa$~~sgl-(obvF?@0J%UR-2QRkqf&QK>GBz%BOz@B&d*CL) zjW2Y(|Lc$k+XBoJVBbQcVX(nwekZQ`$e#=KvFMusqE6>romDZ zBmu*R1P=J^ep$7F3;pg|jvy;qGt4XT&06nTGOx66@SHzWDa;&YeZ6g~yzQ$N4se{R z_Q}avJ}hShlmLeWk_mu9gAn*#CKbv9l&Srh@S&6r{&2Tb0-O%**ctQ~y3sH8>`Btn zc*QrB<2w&W@lH4#(Brr@jylB2`ClKM8zmr=eNxzUCsTU%9sd>-l2f%btQLSw^fO_r zfyylVFXgF!*dw7xuk)fYC4gPI`j;C;JRh*oCOsc~)0~TAr&oQoO+{*4jYexP##v>?HVKK|XX15xEZyrf9`%u5-e+HMN4m%x|mE#r@wk>2h4RFjf!Uyko<6_Fgp)F%K;d^VkJ_g1yYAlXTOp?|~X{->}|bo@C=??XC@@ zcm;Tbc=)zOzqk*zv3fyvM79NVX-7WdSmRmaR^qz>mhO&+mF`#f$cM&<#D~I*I({>R zGx#$GpD>sFIU+d%IYK#nIHEX$I8Rv=xOZ73xX)2>@crQ2ghwD{Td@ZpTo1-vouiDh z*Uyv5FCObh+-!{#j3V37o57puopRTc(*M>Sa2{T@XWz3Jfj3|q!gkIj^4kd8@Y{&k z2v8l09D=<#xOaPoZyxJV)dp&AO&m5~H+6wMRmNAwQ3Fg4C5(_9KpPO+H@PKrLbT4; zNoeh@lTimF@){)q!s$kj$EmnBv~IU86daP(&sk&GZ(0B<+QUsBUh zyLeo|Dj^$^iU>tvV~2}+5P}o1ix45LA=w{i3O4}cO+tqM4OOcB(LvC`b;$_k4vult zUjqcG&nni{>iP1=@ao992n|5E3l`&>&idHqkG~7$9vz&e5q@l28k+&?g3$k($bn%a z8Ofpb=m1%uDxw6Y>x zh|CFYM5<~8w!~yQlrcHW@xiRKk#yEeVpvvk9AbmN%8wmK3amFkTZ`+{wHWe7qYJ(m)rB+9hD7Lb|}Gi|nmL>S*7D&J|w%gI6C z@}uBwNci3f-{QXv$Hn1W>HPg&^s^EAs%yIo|4n9P{if{q3#bCPF!et%(=q=y%yi6j z?EfbdomSG6%|73spc|hEc9KxQ?%A0P5p*~of&D5jbtk;urGInN>areO*O!dEyov@< z9u=dqZu4eR=9UZVVKBIXu;&U?O^O3lT_+rjzwT25v;*~8w~ z6~pL*4O@(`cIUllk9+#U6n9nU-5=b@d(Cr$^Uy@k*XA|e4hP@t65czk&^78!i=z&= zNfF(QGfT&qnl4<=b>}x9-kqHLbl=`r_soLI>lMj?CPP$O#f6c@f1*)~k)Imv@7%pA z!dM+Y*Oe#rgY{oqMFlmt2g3xu##iOskBm=@kI+xhk0~C|Um{;%UozQ(ma?+&vb{w* z0;-G1OpRO(U2@P)d2q85)w3ejvqlek5C3D9=g!LmTKqM1s&SN)$cIplpzZ&`wJKws z&5G6I8F+&@Cj6fr{|Cr4Sv*Z3&Hu0EnfjGB)E3mvEAEv)D!EZ`Bj-rS2ABSids%r} z`M<1ov3B;d=CtO2*`bmhMki(mW;*PohzUqz{92!t5ewaaa7RoDef7zCa(t>pw+&v} zJ+GLHyIfwBdv7pVs&V&Edi$AVXmSTSUn%>5_=|y7{!3)_pzOZ^^ z!#ZO)$JI1JU;*L@Fpyu9@-bkDs1kz12pJ5s0?pc%k`WQ557fCXH@Kg2HlW9*eDGvj5U1O#i?KFGH(JcoMF2VXz-4a>d+JP;YI;R62jgsrrUPp zpNg99t!vS3jJJLKrPInMNQC?teV3o%s}goejiBi#)<+#K=^gyJq~~eK=Q@`~4&%?5 zO7|RYARV$YOzl4N@I(L}8Dj8|PCM5-u49h7 z-$h&>z2hT{AbV!LTGWdA1HrW-c!;t9lAsV-hLKAkHg_I=o@o*HcNyoRo%HX&>2Q z&y;%H_Zn}n#Gl+9D>qc@tIM)%4Pjglz}Did^sp_ApYcVSxkb+!2KAbJ^A4bFi}cjO zqU@zPEWtXTdGbyPVU*hA`M2a+Nfy%_bEVxshJk6$c$#4xT?WyIUYiPmatN$m*$Q=3 zD6tKYO;;Sv8BY~D!N9IAle}m0*gTe@J;WE@0f;Aih?OrLwKe zqeN5LZsCt%Lbq6Wp1Ww-BGbaO!KYbVJ!j)cVT~ytZ6%zQeMon zKzz0KUD$7a7zYDe9s4bF)8@M9uRUlOk#I z;sb1MckyRQdJs54{EQ-TujqM12n@N`vSJ)B+f8~YIGkIBuT5_c`F7|1 zKr9__$Vk=vGe-zYSh@KXbEKN%h+&2>b%wBY#^F5I?V*vSjyB_T>Zt~yboniPPrlHL z<;WBo4@n4RIz2%=WNRihldXewxQQI!14gl=#o#s{Ik%B0 z8xL&|XUZZpf)4iw%->kySjq7fsczAKyN96qLVJtt&cf~hJ<>SJ9ZN{sMgzodLCfnXodkRk3ooT$XxB)+A>NcPJ z)?{yiS+Q0tVrV3=r$-q%Ep=>2Ws1o6$d)_+PlZ3y+Q{SW z#ZBG0YQgBLsfSROLVH5)LD3LYUPt)*G;xnfpeLSzBhew%MDzqtY<>JIoAixZP1i{q zM$}|u=InfZO*7M0ZsiBR#ymb%`P>*;(JO-$%dFaZEyzVFlB-sFIRZU9(wjlL$$&t5 zq=+kNVLgd@AXSeH{j+g6q@!c*dbd2vUW}4i%O8Q-)i-P7%NK(=V=6`Y@-AE5GEKxs z#j_(1dyA$ksP51&SxTNoUxE$mr3_|(BE!PlT;iLPn(W-BF*L1|0zaHB6C**P$fb@F zUm&8;RmWjjZH0O!EzGp)BE~d|a=-tN?2uvgcl$Mws{+^#1Qd>0!x?{k#K&w>iJC6Y z-_%3=Z>%5MH)T|$VdUVM?CfmxU%ym~$ojFh{0AVQE0I;hE^8ursj2`r)R>mWsH%Nc zRC~1eN|u&Z)=@^$;Ta{;8Ou!(JXB><238lEAM>Ob8BMepzU!!#3u>8ZBUi4W{MMG| zfwa;bi8nqcIgT^jrZ5jP6Ny$6 z61p1A9>s*ZulmP_<>~2)q^7@LsAf(ZA;HU9n#y@rSKQ-+|K1XZFd!9Fc zhbmcbat5I9CoxRf1lHqxESMC#$vV(7q9ucKraaWi|CV;5cqZBczG?IY(=+KDDrB9!n|%~F>sSvBC$uP0dEuJjUY$Z6T4G17a8aN-5e*lixYJ>s2b=^ zxV#Xpr77Jz?^1{TV8iMyx^T(O0h`nRs=V*50l+!6zd+AH@lr9Y7EAcYnyAM7>bl$0 z5IugRc{upVdA!<`;PTKs$X_z5wt(<@BZ9@c-OD|K;Yf8-e|x=&QVLosa8~$wRpA6Y zUnQ;= zvrd^%7F*Ssv7jDKQvW)(>Y7Cr0^2Zu)FeX3!Mk`{H*}{;~G} zX=C0KiR63R7{O(laRa}PqUmeH0-0AB)=wknBW~xLj}tOyocd1S?{Q+zcDyb66EsdG zFsO`n?ThF&?&#N^?7;0RDghiuBglL3x&$`(?og-gICN^`gAgh?mXGx~>Dd{y28g#D ze90!Iy5FMj1)YQ0@=sM5moXL8ciXRcO6TuxzPeq!T&qYo${z;V$`!0!Xg(u&oVil4 zLxzG{jCnTK!Mf24>e}HW^2Hs7^v%_!m7aEd){$^RAEAYv>zWyXnq3-tMz%yamXP0` zsKr+h5`AP5{d5xkWaUmM5CwL74$#B$W<%z)XvOm$kMH;oEHK`&JOaTaU_TUMI@-8C z)9IJ742tqmL;08Ol;f#RBBR>O9*W*&dz3wwRC0 z)0+J}=B*lv0!xRXSt?Q}$$=@I47IfN%Sq~0PsUw80hKf=Hr*^es~*^Fs>gLWHLhy# zz%F`d-aIu{U7HxRkvGx3*uJ7w#h+eXuPLArl*2Y&&koyNUGB}{e?R7rl8|yI!l4}s zBxNoWkZJ+punq>zS!D@G8ChiUNE1g~!v%*FYZ565`D77AVG((838@mgj^>}Ssia2^ z2}0Wu9a)sODrsN3P*MnG;R(>v|2_|IiB`;dlCo?hmo0jVHaoh#1M2JlG>PxoMab82 z56^2km&KuSDh{c=&&~-13gRD2>Rb`&3Q2FnuPRW1oNzWy4JM%Az6Im}kK@{qnblQk z?r!dSODyO&tshkKdEVbQ$7tbGVh;S89S5TYEFUnefaOV_uK!*kakA!@zYn6F&B3CD zBF5GdLk(U9eIZ^Uqs`^8ik8nKbT?}2#B;|hs6eWb6w&_oVmZ4!#)=`KT`SlwnmAS_ z-7J))k9BC=IL^+S8`1k1Dx*BlcIw=WZmDBs681q!FGKr~Y;!#skd_CH{B#`o8+l7k z=_d5Esz?xH-6TX8>@9#V3t{S#O$(8Z?xuttJd7{vW3$8Y z)xLy`dP&Fjmr1deY-2M3Jt(~Z$Do0u$*xL{9o#y*IPwQWXQ`VT8 zK^Q^28Me`2Y=KxqJZF~OzzI)P8`ia=roJr>(<}{)f(pyo8-nR{RW`1-)dTr6)n|ae z2%DEq2jQuW_!%Z?5ezaDtQ(fm>{3Z)_8+HynQBz-!zZH2+yg^th?2*X9XcA1tVCx( ziinak@e&swao3yc%{ml&~=}w?0Su34nPD3D~QRE+p8%qg# zf4L3$|B**1MzPKRPq||MZ*ukDcM#a<{+oFJ|G8hsZV`k?is6U58foD6Go(z{kB{ER zx_?gWp6&n50{Hm}-$R`LCPCw3A7Q7mLh!)iEn5865@-Uz%-LF=^_kdWhF@Eha@)0XZ^hvY}S zoaLDF)_@=QWan89v!nRBV~v7^J29hb$VX7t7H5|06=Q}P`&wYFz`ab}yY%q@T%Uic zZEkoTA9~us8>5?}x zOxJnYqJl@P6g@n9wFuu_>Wkn(MJfV)T}_h;djrjMQi`Euy{X7#K5j(CVjaD>_XVZQ zWaN9p^|{NF$LD3!ak?9xhMpjz5CSse+s(N@YTy9`jH-Lr*tuG>X$2|?Dn{a7ne?2RM7={@||+7j`5`!9h*Hjt00p&NmU-tOO1UCSVR;-x3=)5<*F z#WtUb`6 zhq>%u71OTDzvoz|i#Fe`0B?oX|L)<4nv9w2U%$HJY(lNKy|wX)$!+X@Kxo|*7z%Tp z_^Q*qd!Ds)O3nr~0mlxyfzfU-!FK?6o}*?#UL4^u2<$>sF{zGayYRc_>*>vVIm4xj zPNoZd0!|!o-Dn?Di3C~z?=TXrH>0;S!CWJ80ip+Q+im>gOt>?{**-QCo~K0cLPBWW}o$Dq+}-i0j6wtq~YN}Wf8TlpQT)nu?&E#mpPmr1>LG`tw0~7oL{>hoWt@9oFkjtOyQ#9U1mnzL zmZ4FmQaNQ&>7j)!SIa5Gf$PY^&f0!|$|hZ!>wR|Nja~Yo>Xod(hqQ{E6+$XZAeZrU zb?4T+wl>sReYt4Cg3S2@-a(wDgr8k{WI)V3(M%xol@!x3HE-i0@OnmsEw8Cw3lYw5 zFEp)iJn}vUA9--j!d#>yTEaTa-!7j$f|IzhD$j$f`$(c9{9`2eNL|$xd@}3K!S|s~ z$zC>SpFq5;d2x`amigJhM(hlg+x3s!9(ywBV_1Ii9#5{piif zSB$Yyuq3IPRwK#w%mXdtRy!oFy#KHl)zAVTvt%ps5vcuGQcM=+3|+8+$QoyAVrIds z!{mFr(vsUsSQ+SkL5I66J+P%~HKo`}Q`~Rg#W3U;bLVtP3T57_q_=tQHn=McHQF)x zyco9TlPjWQA8&-8;mboZ=}j_?+OD^jZG0@=uWHg^>wDkKuo!vGU*f0cUK03n*ewgYPJuXuehT?rSK>Ta3z(nS2RSbrptfxE^ zh#HmYGT#2%W>v8IjU;F!)N98)%=w#+CaK3~1OG?Wq= zqsIAMR?Q|R;n1)6t)g*r3-n)|r)-0w?Ea|;sH`M75o*ZmH@ylJ5U-gTG zlw=6k+oY8j|B4?Aqz^ZZ(Bt_o7qM%P+_k_LBi>3Z(=nMhen#5-2@%IZF;Bd?dc%|9qfZP?_HO3he!F;3+nR(Uo=B#O6%Xc0r03aPwEt|P6Wp~fa|m!6G*eo>~8F&MHTjV<|Q@R44r;T0HJXHP7Kb1>&e~)=1>|vcd9n5=! zw$(pzpC@QbswrX)dKX-}!9)7~h!awv()ZBC8Q8^qs=#w6;@I z8JW;d>=zi=Fx^en3!m_XUWEAC-SrEV_FVU)XTy)Yo<`F4ywdfad|!7o6G#-mlRO_U z+}wOl|J%4vk_OpZKL)&bH2H1OM-d#m3~0Xf?ToaaYHriAeJIoozA2_l_kB7zMHV}q zSM*n`(>C~}R9|&oA69!qvuc1;5ypCVc1@H%?miwLGB_`Ea$YhMya$J=KRn0dX&H*& zcgD%za!W0tnnz-=L$-roNQk>$y=?T_W%Z%^|ySqI!2)?a`5!mV*H7iz9Ty_cJrjWk4w-YVqY{eA4M`L*{uEIrFm zz%^RB7mvyv|6Nw9y#c@BWF1X4Gw+wKSvOz47SK}w{_)4v;t)u)gjY6-;t;~4Z*mM}&X~DV4sVKt*_wg1hKh-8wUawt zl7P2EZ&P&4U~gIZy;_w&n9dY?^c0oO@Z^MJ`WS#Liy?ZjjIfnejuvD-ywvGD50svE z4cb5^O`cHEQQ7Bn9NgS)7BOpE`SvVy77H&|&s=QXb>xt(OyO`gi^eb$d zfalBtr@NAc{4pt?Z9O@)W&M38CRV@5!i9|?Je~bGiW7tq)ag~39S$limT)AmP-cVh zwp@9`3tPH$=}P?uW@$Eu`xVo26<<*Bz2hXDQgACSF(xdM>ZYNpj;c{h&pq)7YXWhC zZsN~`P6~u%SaI%=cnS{+Z)EK71c5-|>`lM)Eq2>49uV9B_&(w~D4GCcyI)lPjJvS6 z3~Z2yyK#vRh^0El?x9E4DGyY(^;!DpMv#qM!}Ajb^;U*=b#i3Czfinjdw_KVX#1FN z5!`UA{9pRiZz~(u3Uf|{goGk=QGK(` zG*Opku*qyJBf|^?lM3p^@>Vx=9* zuiT4~)=BxBj==`#I9}6sJw?P9Z{FMO+dN_DtFQyKmRPU`699|m31GEn$WIEdl@7YB)$J2*PwvHx#< zK0HQxHdb00JbF4hS|xWoV_I1Q3q>dE{|6SCZ7*tC+G4jNd{KU`7Ch?xpcer`eWzDpJNtKeDZx9v3|sWf zvfEzN0WCk9K^Y0u$?T#Ad6w(xEXAI{kWv+jPJyQ-ECU?3CS_N`=_n$DnSM2rjwjII zW>;$#hZ_rMH65%m*KoS%a3@!lkkz;NL6@4z;IKryz zCj|CTS4<)Y|9nBzq#-qc`Dum_HZ-xEM5)3u?_fkAv2w)4svmul;THs&pu5VSGO`Qu zmjHStBkw`2_`wSP88%dUTC#|F#BoD>@I$uWa9~_4=K}18evGXuzMG^Mz5PBFM0@19%^?0YO!F7b?KZ#14lDk<) zDDjSIy!x=o_Ho4h32}rhXgGt0A-Z1m?)LVr-rkPgJ6YOZU2jhHZ};!_$6wd)gU46b z?}zU;syn(jHaE|-ygHju?R)=3SzJ?FJ9D2|TgSqRUws~~gv;~8SxR|-+J2r&uZ}OD zPp;I*Z)=$rH?}^$Mmrr-dS7wBjr$Bjq|xT@CP`)+A*PHR=GCM@)Ex_qMtAAO0DT1W zNF5_PvEpE0BjF!tNE@O-f3uCI_esM*J7nopfN9xA*~HLVh|L)3rv!YvW{hgfA0#roCCPQ!PSb&KS+fN^{PBIXYsM3WHKZ#DO^{w~0sssbWm^PFr-+&_a zZj7YM2Q3rYW75ORc8xq^*J#qv*Gi-tWYbS(JjPw3No$=oNO$45R>s!Jksc1w*q}+7 zi_&ro4P~f=G}#y+5hEvN`kV%{9X-|8fBm7IECYJ^&4OTKL;{8#Hv>Hi)whG_mWv!Z zp@?mBAb{*q!I+ZoUFcf{LiJPp_gAzt!Py{DJ&#o4;Csxw9qrktQFdBM5mHPlxzQVK z(I2Db@TMCah51l%2}PWzd~x-e1{`Um3!(XTv~6x*xEPN1&P|;gcU!y6R8(H??k(@v zwv%kvL82kynW)Fi6;)K+>!A8@Bb~0cx0g4@>zIk4>B@?&PNnzv{e|oEm7VM>9_yQ- z%q!Y`PrIF`8^yKENLf5e8h-xw?@?4%)qe-hzFj(dKik|L9Y4THtJi8VI4CFRC@&vkv94OFJEId?X+tf&8|pq5 z-^>(m9_(q+-!UCW{eF74e{cS0Q{6o2ep^3`zcjt&&`MI3N-igB-u`r?__oK=6Mql4 zS^XwQzQ)=SB@}F)zeH zKK5rq)=728m0+Rq$tBYm>|&e4Nn5e>)zk52dpd`FXb9{x#=?gv4`hEJk@b~mTYLK9 ziok|%yR#1<^LS=cp&2inmDBPpM0mh777#l}vo zs_eRT^jS+oW!AS|t`>$2QYPBYlrqP}j_n+ldYINuCr>84?&1Cfg#)wG!QO8jJ3TB- ztx1;)HFD6bdOFJ}6h%^YvkhO6`y^E9Y;N}KY};%@y?wg7v&*7wTlZ{xHNN?Hw)3h= zz=jJ?_02(A&u13&|BmH>L?~pFn621yJe@PLgk1p>kbVuTq{{#rgfs|fJ5pf{8j(rj-xt@2j}w!ac3 z_&mSP1sm!B91ajm#!!Y5e4QU=pDiY~5Lduv0$d@i-+?$siA=a*!mNA+9&u%0wneXO zXi|iD1bp{*3w;XRsf$mh-tzv8#B^(bl2I(iH91+9F;N^RSGtYCOJ_ZTwu(fc36(nr_DSxOj2$3xU%2_n2sN!Xaa&YQl^DZOzuMvu)@J-ZBO=Ff9x@q9#}1C4URz zV=i$&xQoogj^kq?`q8d7v+&^06gR4{i@2?E8mK3yR^fTkUnS}&8$ZH% z4m6I8{<8B$)c^5=0#)w+FHXKGNRue*wp?9Cmu=fNzOrrGW|wW-wq4bQF59+k>-OCE z??3Y}cP=6`&clhw%$F0n*IIk8rM?tgfr+UcS#Zuir)xm0~$&^!yu-A5b&TOm5Ty{%dkoWg;QyTBZ1 zso<=mUb&nf2Mk#_h5`Qf-Idu6xX`E;KWdQ0fENo+EK7CW_T>9uYpBfn7R1~;aBR(S zf7C)qx$uvtX(IYSq@Dw$6mXh&t3)9d`)721YG4*AFCok+OVO5TVtmeQuPrjT{wTo{ zg{eRjdyd=fAIl~gBYdmCdQGS-zM+HeX7bkA@b5_cd-S7pchu*PxhfIdMI4VXxtrbL zqlcYY_^akV2hKqKE6K&Gnrv#TC-;%4eIA-04ZF_D(U)0OlfMj)^3_#3f@)@|zO=-% z3sj~uk!vFw-BrSHvd6X$?B4cCqtRE2j%Tq}AH5W5K7HGs!T=LpF*;$e*+t)P8g88_ zwewU)X&hjyS67;kw#!F+P0CV&G(OV$hL_Dfje)9<6gos;HUI)=x8mfhXi{E)$IE>{ zR7LPCe|43!T?7ktR8$43E&ZRv7=qPy`&|HSNC{T()o@=5WTbddoB1Ix*0w7)W~tgu z(V7$ZqpfYyb2qh#fQ#!VTsxn;t@Yef&hb%)`X!I_Y3AkSKRRpc<tD8i-Huggb1S${iuyGrZ|%l2-2k}qSy_@1{iQCA`_lx z<)gY|^A+lGYVwtP)Dsfq{A)9)xrAah8{E)Xv=tcI8>lO=uxiSn$Lgn}fT?<*S9EpG=Lj@5(HPn@ z)81^Gz@EK4 zJZ_`+?e_Y0clX)(&R#7_5o#HJBt2^4 zVS}u-)S9aH?1sO+`Tg>jpddWwj~y-!Z(r-&A^h@vvC7_m4Mk{uW@o2{E^~(tR?wzt z%2yYBq;L-EA5$wJAj)n1F-&?#LlI%A+yEbZhy?AvyPRK!hTdUGa(o>D0u~lViwnJr z1f9JviZ&R?pD!RCD^uPIz$%ENM5!}>2x|yfVVSQq1jv#kP-@y+D43X$!-#TNK?)CF zR5_tpkfJU34^5FE`PZA3!tpjwcu;9cS6}5&(Sk@!M{)`I8*?0&pP#vzAZnLBk=5r* zxHIXi_jlz&S#l08`0so}+)iQ(O*tX*f8SA~95eM(9o}7Dp0CRRB4yHTB8j}@M>z{t zPI${lW3W@K2(} z9!4F(eT-xHv^46dX;i@W=Dc_j(q$3y`z@yPu;hy6l0NFar?e)J`bxyPCU#Sf?@|KH zUY+(d=hWry4V)9$(pwuuk9du&Evlxq2BHX^FhRlH9xivtb*d^(lMRG3mP0rCR8NrH zX3A$!duEQ73MZAOdva*O9~6F-smMAFiMHcdtw%@7I2JvOv{@7_-Dy+@ag`G^=bGSW z7#CPfw7i2ZW3@l2hpw7QCF$k*EIBut;V;k`caIsDHDuKgrca@IS#7KhtY}6*8(61N zxY%k9jWM3eK2An-Z@ySyj>+lCYO8q^smKu=0&CGSu+@-v)Vjr(PtUScHh~wA<#6+Hl{>$&R3yoU$eYg;7}mCqxFmv4l(Cyu(y`K2}H|Z zTteb$v0M+h;Z}>^qdL=uj_FQuug-E8#jxH7p9Udp?UJjT-SgsdN^v%V$y`A!vD9_0 zp1E{q!lf6Vo>uHyHC+(y7%y_A!?a3w(|2OH&U`MZtknq>x6kBAKHLw={(a+Z~8vp&@gq;o#o1B6@BWrV5rU`>9Y z7NZ1xE?s$AZxJqbTof5AolNzr&z3|LJ1NF;7b49z_T5W$`pu7HS(ve8_je?c)~Tl? zQFrz<1DUJTiXk2croNFQv$I>jymVo5zUOPl-e-S?c)0v>=fQ!VS8U*&?pd8Uy#9;1_x}%X$!nJ^yIc*(! z(c8fVnvc*3SmEQ~^47qt$)F|+Ju8FY@n}=(po1kC8B;(Ft%79z^)w8^{53*RnHXr+ zF(6C>8PH(wdX1|FsA$^si95JSqXyP^G^6i=wG_G=j?s*#2%5JC;;Kfa08ddz+b4nHPdb>~sv1>SxU~BumkZ>4f1cE(I zh^gZBNRdGP90np2G8GcU#;F4k-IaIh5g<^fYYF%iPl5#tA-=tDh_G0^>`9I&g+xd} zlmSF{d)~@K5I>g0kr_cEhtJc}rf0H?Kw8z~!%{(|C779sl?NF5F+?1@j7O4D2WBk+tI{EdIpLWn)_UCx-28>_s=KA5wRTYO$EwxeQRbul63(uF@8D`ZB=n0x z@1rrxxIvwU-EK>NtB^Um(9gj8bA21!j-^gQV!~DNqm4&rNkq)%XFaqVQo0S%R*{Z~ zuNeu22xR=)Aj&$i%$){n#4mYOi#$PA(8NT#H(m&`dy;Rm*EkHwLKJOKVJnNO&Cqzy zKLAFX#tB1ux4NpmbNec~u^^?gAuK?D)P<2tH%*~GgW*U-ZFN++;I4H-k+amjPVUh; zE-mxQ8g50|K0Fe&&i2zyjgnq9R@+(99aCW<1L$PM{goYUhNlOATGtgmR}L(4l(_^rO}N5^2{hPk$iNQ{S9RQ?5OL+*Ix-0> zEsnBhhQjy@dC4^h(ZMV`7oedSzOe~Q9X8;k6*FYaRz{`dKn#p8$;6dx}&k;NB{O|}~yH5Dy> zYE>=rJe1nEClH9+?c*P|yw`L~>elPXVhRhBMi{(8UgW9I#kH6OfX zh2WUB+m^9AT7Ir2t9*TUbFqD8>F!JVc$W(uKOf(Kh-;i2eBPUPbYbqElF_SZN9x0~ zD_Ope9NjzfB&)MyZ_NA+&C~-n-2`jP`g@Z`ZEzX7u`o%^nL}qfttji(&+Nlp^TWiU zzAKBqCW8Kc(6~N_{rC2y+WF-A^*k!s8ZKw4r@8=s32Lw_0^!i z-*j}Cezm-rbgh5*uQ5l*RO}W5e>>$$!n5Mu;;ou$+wQ2a@V80jER~md;RSf9UbnkR zXg^mB8x0vpSgwRndI=fTx#a3I_>|c*zEn*1N!9bPl?q##?A8a^VJtN{Ir^batduN( zoKs;s6R%b)(znvdkk?$JLSVO8v@}=EMGZHxsVdqqn%aH&*sRb^q)k0R+Hlt*cK7hUfK1~ zQZwJ$`tt54=CdqD@4md<4xGw~|DvIENZ_~Oy)aq=oylp!lf2Z7{j^92eYT2n>3SJv zDt%)nj)|2`<;TPK$=X6=wTdjH*O**CD$>@VC4Vr17AtP89>J{3(Wsru|7+X>2hN_# z4N$%Nc7#UTd+d-FHz*hyS{#Aj;!mUhfQ+H}r7C568RwH=6kw+b0M#W=oP&>yd^Ym~YK?EPc^-TJ-#eWK1IT?PreeM>zVgy}?}f zTXM0NpTdpuOI{X*yYyI0U}NRyMH-TlL#}7)&!%%e7>^3W6!fp1)vkga`c~SsINf?K zRlZ^ZTXHO;_*0N~DGXS*&A{n6$18|=USE0i$+pEW&UQo0uuqsjO(&MwyP~M{mj~}! z!ro{0f(d~R17R$qB5Xm{|@t}T(K$LDyzim&Ga`|vZ8 z<7EX_Njx&$Fc^RVY7vVMWLd&k&3!CvY};}Ki3&-1niQVD5eNJ9!T?7wNLbif@yFC` zz5CSKv;=aw?Bs*=xv}kCZ#}CUU+omOwWZH$m4};qiZ$10cZMEH`eyBW>)w67D;;fG zzxEvkep7jLehr-a4`_SqZ61A}EOUI)!?IB7OgrEV&DsL?DcqQfE}{YR9jgK%zGi6r zJw+T38+mWU(fv983U%I5J1;kySc_Z223-zm2iySAX)P;Y^OKdeJN#mNYNC)tAtqKf9vpN~+VQ!9BxXy{q=U8&Oa2{g6JWwiouV zxRJKk4>v9Ow8G^Yf7uNBs8C};4n6Yj<}8}BCwZma94F=*Xg>(|w|RTq6n7=1-hHc2 zQAa9g-y{nbOmQb3T*Qz|*{%#f7B*I6XL#2~b2Vrudegh5E$*&F*KQV3gfo-h2qPcD z#Ar6x0H<%kkUNwOz~};c%r#eabg3^KK&woEy{N-Scy;bm?(0-5e0qMUQjG*bNF?fpsv*tsR0(LdaeuS=QhFUtY631AnCN@cCh7 zL@Xp;O`Xvs;iVIHwY!2ZG$9+Dx`nVjVfT#Vd|@P#lRRL3{Fu`R z@gqaS6o9bXa&rYnU66F?sh0$3EZP~^TibQW?pkrU#dT=0KwiE$K~NsK5K z@fea4krDY9GfwsZum_x(Mq?C%t{ooDU_UTWIUs#nu*87c>z zP?L$Kjr?(iQWPLEcnHdgoF{tFW@yh!kf1Y~Ns_Tl1$smVmI@(2Mt#W2mVmuIzL1GH zAqG_%lZ_0JKyC_?p~CESUV|S?VBIfR5-gRyNmSuiuHed9ri`3F6p>B#Q)zUTjM8l^ zGtWY!)Ltqm)7cRpB=1O?<6Fdk&~_eEjB+m%hOi~fW^x7u2v-UL^f8^F02F8}h)0BtR+zd;JBXvnW+K z$WY;B)lA4-c|VnqRmB-b6B0ngrx7ucPc&5I^0-6^s&joU;iv27ro1hCK0fhhTrY5b z%xJi6WwG$f$&%Y`<;{Iv7;@xb$4)ME;coO`$JF#p)z2;9z@v+Lt#%%rc-pfDRvd;u$cPV=zjAzhy1akv@0hO8(IbWW!-Nb1iTPFp@W`Xe)vQ7R*Y2)4e;pg0-;cvo&P zcQ9wJ+%?_N-)~lv$~Cn}T#N7jq77Nak0rlnjhp!yhA``SXqaQ;FMijrx=E$bLiz@u zATKObQwz{;0zV+)j;flVZ-}N<(&lKB!D){exTZjwTJ3+W1?wNwDEJ#{w+-g#4q@Bu zx-!-!5T46*fv&Pv=A~O4KMLVc{BJ9v@o~eCI5?s&<5ew?lR6=u5q}^3&$W z7L|_VtRQ&b%Bm4zoLnCl6>d|0fGd=7I9t+XLvqgB>=grU=k~ZK!@hh>_ zwa8Tv{i9Wt$hlKm6|z(ZIRG$SM#Jw&+8KLuKT>I4s0-mKTpaEA-y_7cLlyPe-(0IQxi*rjCYUj7!bm39${I@tU5;Io29zdK2vW_PZ>IajSAsvL_^}R;=;$ z;cM~h@St_q3UK}OnA^dtAR$#ZgI0!&(p(}#eInDW&)d}+*%ND|9k-5Aq3H6Ez z1d0VM9%hwRpu#oN$&TPA6Do_^JPC9f7)arMCttBFun0@rUe8PDM-{X)A~yc_rA33u zq*Q&6^NPZ5HYS7n@y|WXmW0AIw=BiZ>a-=LJ9U@cE2iUXhT_!AyoJnuA|3_L zfdd>O#)I3e&U$kRRu_pTV&o9)IpQT$WC*6^zX_1w$gbK4h;?mPDfH+XMg+=*>qexQpiS^qu2g_(j(BUwnMEN2%?uDvZy%M-d6Bzh!Iu?$I7k zRcm#S!N+B}=4&+T7M250-I2)V&T%WHw+AZ4X0>vnzBdH#cxlqy8j$pFmVk(#;TbSy zm_HI@RqZyMx49=1@syl{wKlKYpW{Kr$aFy!fM#xw?Uko<9PQ4Et^n8X33$BV)?X;b z;FW=eST{#KmiQFwd9Da!OIx0X#Sfkd$Zayit#OrDy)@h>BM``fU^{b8fUe52?qit( zj8?4VZ&gz0joLY1FYb;^1pnr`DSpJ@6uh^ZAC=zn=4xXgrG0s=R%l0cvxX-GP}_t| z?nox_TOpudg!#=mKNO@?8Xhy2Y1cVji1m2C6Pu_9P^~e1WDH5y6ReA3e%&QVK_6l_T%xUX4Ltv0yj#>sWEmcthWy3IvgzJpeS1T12t zWIrRAQ}Tl`^7YF(e}bym?B3Ownz)rMK&kG{rIy<;#k6gXB`e+sy%13;QGIiC?Vi7nS)|qUK?u)hzkHVjd#A1sEE-xcPYgP=c}9O*lGv6fUwM)t zP!+;|)bEFoC!ko`ku~ zyY*_0I{N|s?k5lRS~jZ+q%=KX;uJ>&x^np?SM9_+bF zKJcrDhn>;giY7zbEzH&xi)Vj6nbhaxfZ_eAv=vT;W}u3lT_5;Cc4SC?0JUgy{T`}Y zEZXbI)H>tR-Jl6_4)i+I@*N8V3bba)_~+k2x_?RU`M-yBf4hRxCbnkI<^)U(oNWIY z(lz~sbU5trS8}t%Tjv#Ju*M^+U zkz}VIS?Ra-MdBo)d_~sDiV~`vDvH{q&AJYYB7^pq;p+CyB1`sC#D7^~bze}pSpjaiz9(vrArVaExiqBTc2Y*Ani0Fj^OijAW$Mo%)U^ z$~CIc_$Lk9aV*PjQ6J>NL77k#cp)A{;G`HS!vVLbszTZX#I*5$5^jMlAn^>!5V+V* zqHwNdSRBF~ipU|%H`ER15TE`a?*p;JsOrmuNe!c{AzRUCK(PavjJ4Gxru@M*WrXe1 z*+Uh(ijhDi=0>q#h(h7fpl2}Zqh8UZayF6=YKp1N)6kDqi^^nxCPUNcnIq{2B0L~z zGGQonr>O9n!)nCFDKwxoMP+B?)}>bHBHH)^5}oMSr$*M6kEq(FL;-zo5h|6TF*j3> zDHih#VNxC{gG=nSvY-{P>4%6H7R!KsbWUr1mcX?fr?3%JK(M?jG^D8ZQr%vP7jdU( zA1K$|n>p}^v*TGdIRFc8dy!uSoq9W1S}%zpY7duwg8+pI)A|Dwf&Avcr5^GxS#*io zWlaL#c7L*N)M?R>pl8?9-MIz%?)l#C+&mgT&us0?ocyii@%3?YQz20E(e>(iZ+P$c zZ2Aqol_b%Bkdu#KkySstV&%G@%Z~n@$-jN^+smi1b8X<-#_9IB_8sFrbG(w@)@)T* zN9Q`1pMU2hk)z724bp159;UvUxi8gDp<4e%+~DJ0Mc5b$ga6~@=S5C`rqqx%dS-3S z@84g4gw$Qqby)Wx9-?e!DVLFVg_)kp(hk<%EjL*d`EksU0n5^!v4NI>YFUGQ!Z>`9 zg&Hv@j`ji~>XT8pq`^)|T7>aP31W$WIC&;w=f4ga&IIvMylE=T^-DrzTVWEO$f9MO z_6v9kEBB!g{?H(ZPSE;q&{R{f#OCc-lNPCjY#k)k03P82dpBbTb?npdg@&3|?sI*? z$DJq0CQUh8#h`tOrY)ws68Yavw*$d=d-zcnR$8WdtB4#P{#)lQA^iOTH00r^c+ zC9%qsKy+MtzlMXD4oyRxpGaAQRS6cS64#f00qgNJq+JQW2@y<{=$iw^@S4t~0wT1X=q7DhMsqiz`Rf4i0r~4L7 zCG2X67d%_hKH9oD%C5%s(Bu>f8q;?F1?HLnGg?5>9(9*6TJ@pCV3Phj6ZDpT!`!ug zwLa{A_!KfZN;9YAOWV@Y4uSn>F}H%_BdWuqfxxWBnFwte(;1d^1i>vB$t?X&L3^|bUqlzZ!9{5 zMcT3Ed+lJi!yln#$CY*4k3W-+jg0J4AuhbnK}}=rb=GbAPUC;&ubfrd%HuH3acz?b7eqTjSnz|)_d%X@Hc5beZFTcJWwaW0= z^h6Kq>1uaxcCYXLuH?OX?#%q|@@;&-!2e~IS4;QH>+}5ZEA`fIy?CAX)C;_rfC~9A zAPjs8kCQ!TH@Kq)&ssf6GkXg>Br8H6Q`F)C)US7c)!%qiq{%?R@_rTX6)e~j9QAr+ z;ScYyg(!?=Rg(}Ci6L9|NR zM{%;4r&D178W3 zdYS?sIPcX2)L4L)WSA})%Y@PHbICAJQ;(auD$53adGa7zp1pAOpbHyNR90o+EfUn( zO#v9x!Bd4%J`-qn@aDnZ(29}w8_f5!f=6ZE?9<#e&lPKqMf6lYo;*gNjaLyRv53Ut zdg?gBGKGLk4gDnS!a4iZ0Ul?hfp@{#+W;u=&$F=Do=o-skF%zK^ zOjF;K7gq|8rCI(blq%gc(0&MOh=U*;t9*cRn(*}3HGqG@w|^V&>Em6Zl&1jN(tD25 zQ97J6ZR<;AW*}o> zIO_US79)Rx*cY-s&~O^Z-oJw={}KrJe+^NXIR9I9Vq{?ahw8-4^1pLxT&UaFt+OHi z6`k6EVMMoXUqDcRFPt@CoCOsirPPS=dBg!hoT>!WD{t4&@~%v!js+@u=okTj&C@GF zj}EV?(FP_ejZ%&sGAfRZu7;&OJe2kzFJ&CwD+wq;SqRItL>0C^0U||XFgrveL}Cb8 zB!wj;(TjU2-FS?CAl2?brHBZjkueDc*Nhc?0(sL&!2tL~!^BGlqn+JI;Kd9YWLPH4 zCyXE(=!j7u22ci*pez)pAi)E;0*?KxIP>|0W^4SY*DN zygSS$ougq74qkl4vT1$GSATE3rB^MxyoZSl5iwSA5`xfJ#qPTrvkC!mLlG>1?m_t9 zs1qbKp{uIW_{BLAnhTw1An1=i$Bm2w*v6|uC|CwZ_pWp_bheu$18BEWv}TdO)v{{P zma&r!9W-m#Pn*8Hd=G@l6_J3m^nPC$mqxBzK0byfzV(!kiDf7M`ds1V*V;%KyrRVW zdg%E()M7&ZH)B?33&qUEsZ-r0mac^XN5BS^{QO*{Jch==O!sCmBdoAPRvFpX7yQ;S0(ZxZbn*3bmk%PZFL&nRo(LCvONHmr6 zuFpsPf(7ukxqE-qb79XL5O`YoWXk-!QoMZr=-K_1x_l_SCf<*i`m_pAgBk=c@>uXF zs6mDv6TJD9#L$&~r`YJ^X`$W#<8#cjzdxZm3}oYL3@Hl~ZjY)7ybPR;_k2v4lg=vP&x}7 z+22ciH|le;BYZ>Fjd9>=qE;od1TGq3+TW2dfwPChL=xu4b&UvsnKp~2@M+-iK!N@h zhFCza{Y925AA6_*_r)8$8Ug?dWKZmvz!mFpLVT&+>8K`#GFL?W)WpQ>_r6u6jv+Uq z&69=saTL3x#T@GjacZ_OGB5!L(EO(wp-u);5$;c~_#FtPT5PQX%xGfAZ&1YHBI5f- z0o{?V@4um=r36Na|L!02pW|Bo%l^gm&uI{5M&|!ad!jTn?660XzNo=pOg{5ewjt}( z1yW3g1ujr&hDYiJ8OX%vfP~?haj2Ghju8&fODj0VpX*qUU{;-+eN^!bbV{84R|W}$2_A7jbihu9-DZbq+iP0g>z}iQ+z8?>E6Kl10d57s*MQ%E z+OSn%D!-(ad@3S7qvjAUt{Ms!A6@c`!x2;{i!;w`g}+RC7zv!gVRakvi+>p*1c!+4 z{z+inM*P<-21AQfld@m&yrj*aCJSG;X`9;rbt zixu~57GUk_4=&-R(Ac-RR%k$aV3KSBJ5y*NmWsERD6tP|q8uWv=BSZ((T0-O4owCl z^>&ZA62>(z7NjhVQR*!U( z=$`vU3YNL_isCir+v{VXuQk{sFjm1RA`U?H3m7$_lYoOND;4cY`WYm+0>Y`KxQjpr zKp_E%`7eE_A)R@3#$|EwG+oE`s4t0lYJK?TQL|ZByP$=r5~=};q<{Dg)y3`@2BVY? zz&{x1Uf>e@-z{c~T?0YS=GdE~6o3c(vV!Wd6;_^Cwd{@4vHi`D7eA!vz^Dyq+SL z&m)aopjiC|Q+gXkG3q zKm9e`HoIO1l7>jfv+B`eoCYgL?_{g#d)6Y*DX%ans>KX;=CZ&sOIrX2)>VdZYA8BzD`w#w*01e1-G9YD;0XB&-+VPt*R)T zn~s;dOQXe0Es7%+ZEhOR}dC{8lz_gBt^#)SoU?Ac&Slvcn7MX&u4R ztc)p`^ZPQvuMIvcu^W&1T@$!+%JYSn^_E%Wv%@Nq_Ir|nV9y!7@50S=!j#!%Xnd`$ zW(o}=2_ZhstBZ$&tpmOVbAvs1#L0XGa}8mVjd87PHQiV{9W^J#>n$GUk#JoW8Ss3m z-+an866FjI)Q6lK5gc^&*u^EowhJQI`Karx8_AWXopftvZ9Sjke{k=a$ah9?@2QZ) zF}r?kS&!9JtJ-f`Usr|-V<$w`Je{QPFaD`QMQU^wnSni_RE5~0&gk6idusVlnu=$kuB2Xvp6wxyjuf^t*y zrdxv!?wQF#z<7ttPJqo$&^g0xqkV9o{*#aOoC&+vr|Bc?oZ@eC%)Su`fopxy3+5{k=|03CSg9h;7x%KxvjV!@v9~mo*fu-3<-s!tQXpY}gt}27c{`>mOr=RP0dO0MjiwXlV~(lgSD%;1J7i7WUvE^qw^F#|CG# zi?^v0!iX_wvSL-nXJvI`YU@3`V#Rwa<$19M8nv-Kl-{Hhb6#AwkX_N>b|rOo_4Q>% zUsvpjMo>)8hwfU++d(BJ>=;Fy3syYL7FdBK%-s+TeY?WwnURX-8opC!K4wuvnwb?Z z-+$_r6WsWMtVN(j76sAR(dR-ub`ibLI!W-{GA`%_7j1Xi^Y*5^bRVvceJJSq7VQ_uUa2iz!BCIN>L%?5U&S zdq3qTEQ#zRVe9UZmtf+8vaWYcR}elw(YZRZ+}@xi^HQrI%!PoPjv(^{7n6s55!&$$ zy^VCW^!Za&m%_USYbGx8i%kR52VKJI;`zj@Yf5`2N0JHWZ*u8NF$1MeG`OE@8Y97dX$qe0R z0J_MTPmg>OSS~s#M4c$9Uak<`iw=qJu`iUu6e4f0r;>Kl;FAF~`~Gx*%$ytzR)tv| zygr17YEWcR1XMeig;^Y0P7FV{PSfGQ4{RAO4q?iYyS7np`Um8zuGyVk9)z|9_(Tk3 zS75m*BLHX@RgBqeq>9G!@Xy{#pGR?zEt|S^b#V^Anr7E0IKhrNRQAY*6L?wnTfM*lAEdi^xYL4m{*D zly3%_ECMLU^Dbw5%{bfeQNng&L6NlNNI14dStbRCL9b~m0%N{hQ1}RQ8v3(b z(t(iJ#2Nd)p9}^s8))XY98z;`UVgoKg*4D8nkJ!`ORXGXA%Tt_LizJqy~KNCKP}~!e7Xr$JuL!L zmacwwC^t%fNm3JVB&|(xWU?XrJF<Q7XXrCjMEBcVB4U4J|Xcl$^$I#5}d7 zY-}DHKe=S@DbB@F)1AHCbyXal99rC~jTUZ{pW{jEVCc>b5R>Auj|4~~}` zwkxGSIvb_wI|*@IfvEmfK*A$m8llM%zdhq<=n(^(_*$ zv1Vr*lQdSJg-QL~EM9n+Vc<`Sf`SKv#krr)B;=>za8uw)$x+ndDi07l5zoIZs|pa{ zIm1r^h2@Cec4Er$@cY#w=J~+NSSSOG8t0PS>2?On+5!-yJoW*t4UCe&_%(Wcq2xL3 z1e1zrZT3aF5umZB5lT35g=9WPgXu>ZIRZkg&ktaVC8Q2={XskXZW$J${ReXl)jmnp zgOodeJ3+AP>wyU3$iZUiPdPN8ZTb6t$-QIqKjZuemDDM#0KM84rXoC*V+_PdJJbBR z-QVmhl`EI4{9mv1KM?H+^Us>zrdKyt?IwNh$LZ%~l3YLsQQ1xh$e?B=2-0o2t}DT8 z-Z8$`6p)Z^=O+u`u-~G7>>smvm+HNDKR?={ZiOkiP|1N;vUkq3$gOXVde#qaU#+hj zfq|4t67M(G3*^d&QQ1_8NyQZp#nTQM&Z{@c8r^bea=QDDiQjCl&qh%4ww3}VTUwG9 z>to}4dckVtyGv5)hD93}No5{bG0B#m_L62pCHW!Cl*sLi-Uq!btk$}$U%na2>gmzG z$vGa(6(*G=sb?!1DoP%do1nA*Zj7B2)@QeAguASP-|YR7oDW6#qL|#vHx!52+O%aROW%$!c?jxfT96U&CyJ;l+SnpD93@mN1DWq$CsDsYQgmoi^dgxKY70kASWY=P#H4qA zxc+*qIq@Qqo~zr92(EE7;uN9JD}QiT>*ozNuA)m%_$bz=6y&-USF&&ac8^!wBO4YW z!+F>|9$2is*es|_mGE)wcv#30HzQ*s=~Q2ALQ?j%K(}(m{(4q!HHf|zQw)~!q%ozZ zjJ(N~c?;lp06(7V*=bPRS-@Gd_!H1w2+9=eQiR1!baThFsA01b$W?Y7Mh@Jv>x$V? zv*n$?WbgF7T`6~bt%2S{0yOph+HGw8V+=PP?P9<*6uQ;stp2&QXkKQxtBfYvLPhWKOUPR?Of0hfyFT6Qv zwWK*EA7x=U9GkrO&E@kO$P8$r4VvFAlO@6Z4y1dhu zGdYkbRPZd_E4Bb@6XarvBip}2LEu@oR+Qn1zDyF-Nn54Sh}yPiI?-Z|*gXrl8PJ{U zw66PpBJ2|h)r1t>^;P2IKGqh>7xfmahmRi5KQG^g=x&b>g!ib=m$oMB)6a$Q_S$r< zTX+4lTMgpt(j<`zXE14p5F;A?dezEZ@P6MT(?8y@2EJs-6=QI+pscI zgtQVjQtI1bv2!OQa*ahNO%BPuiS`l{$iYcDHsi;GZmB>`js+a}fLV_&?k={8I4|aY#=WOGPzVfuMhnReZ`8r^$ z`k$Q}tC_gU~cI-E+REJT3X{zzkgxaJl||F-kVy z?lLqkVOI?|ir*HAO-DW)A~n8k=iL^<)$rJiD@)AyODltEhT|5w2~F#7h-qpuffvin zfXPk|oys9%k_s=e^yrc#ki-e)gN5SIbt;aBR_Exrw~|pu;!CuZjpw#L4=SwZUek1i zVjeJ1v^PT7E{^UBepk%Gkh;$1J+W1)n2Y-&6EGXDLoVWj|KfmFEEX3uT}@iV(M-%w zzA#$(Ty5H35!M4ixRF*Au~$B17FRi+)1s)EXG}7)dw^(yY?k?v#6b^Q7P(k(zS8M= z?+3w{lil4^@+kN^VWHy8e3M5e%lLKOvuBFyT7^xu+OX>UC~R)zkA02yfYy%EfB`-7LFPuFdQ#q)PX*!oUkKT`YnE2W#CK&KdN= z*Z>{rW}p*z8d`b?FH96vTFGq7a_2T%)w&XlJn75-yPJL?{r*eG2W#}+Y!N%#Z99JX z>~Jd+C+t{{kp!wBeW}gSMPb!nejb$3Byx+1Ms4zLebfjBy5IDb7-}0O7q6%|#u}={ z+tT75DhLlxaxbBba7^0#<@eu4yZ09#(!W7Hoc|I^`~QG?*#FsR=V1IlDNgIQ*#EHN z@(m$0c>D$u1R7M;$&ayF5t?bDgjFfX6irUNV=^qU3D$aT=^Z`}z0rmur@XjWpsUV%3tcp6gZYDvH^(==ZeJF$) z40~xmBZ5BP-J9}V3>Yi{r-T}ld8CLVBXGV}VISUS++0arvd$neHct>3IKW(Y!qpfL^3KM$QAe}0aSksWnw^Z zDG12{GGuSj4>pWAi5BEKiQBpXpxPZ^VZ>t9zmGEXfx@%FmLM^Z%@HMJ1;Ry^Il!~w z9Hj*mp<3g&{3hFs&!q#1Pu1Qf~V>JxBAso58hCoORYB7l!ulbCXS%Re7 zughVjEyPw6Bxe+un+dUB`ITQqfRbmSO-dkg_oEDpQtvj)G@QZbsMVqP2s|wpuBAT7+Q>-h3U!^Q@zPUx z8FxJ^ZqSiU2X@PTx9oz-JZ58Tj>;s#FxXt06Du(}f60PH_E;qhtnH!Mt^~$$UHP#} zlh#`(u=@FUoP335%C{opv4mIt_Po;KeKcA6!qLSXapq0A+RV2;ZOQkw38Hg6*^Vt) zPN2}vpUoQKltkW1z_l6yPPmF0u&~{$_&uFiYevr~FC5v~Du}T(OFClsR;rhzJv1uT z8!rFAS(xGKquNSQs>*5cp%?hrdgh?iFaejNcu4nCjgdpC$urS#<|+v#SjBy;CmWAW zMmDm^A(oFnioQk+>Wgi!L91iEoqEKEZf* zddV<$rkIH=?rYDNmCkgb5|t}IMl4B`y=axQY!D4V9%cc2gl|)COHWT^rV0=c&xnx@ zUf7_ot;fLCXbFY6jJ*x3szf2jvo$j4xC(TRevXmH5dl;FlOx}F7SGR|a4pkpn@8MpCG9+&P#7}+MbMkCKnyb(L!$?slBRJBbCVP9dK%Hx`lXmYqAf?j3H_H zo8TE^f`*MFD>ha@rU-uLA`LXoY#c@5XVXHj16#EJp10;)SFo!U`x+AD?B%TtJpAw+ zmy%Zyt{jr924rgofY@xgv}+!eQE|GGd% zrL^IYONQLu9+^eFESkj&=WYLx%VG+#%O`WC_yQRdgYiiEZtIapN4!uD>}#kv4dHQgATy@%hUtb$nsE%M_WR7zk`Rhxi=%}7@!jHM zRD^~E7v7+LDVsC@m42MhG@9?HncI*5!P;9u<+UX3!nnJ;L-6445Zom|aCd^cySuvu zm*DO$L4v!xyTkt`=gj$L?wtG0owfc|t7|`9yWh9BRaf#YtbsaR@z?(cw|koJUrQ7INsb)Uc+R_H9scO;a{?8vx8|C z7SP@kSWlB|K<>w*2Gd%}6<^-&YJYn9ui=8R!6u8sjl zT2yR7Au1(;^fhWBij=~3=V=T0sts}$KY8Iq)39ZNuL%Fy?W?y#dUk4x4Z*ZzhfF5=Vr)l);JW5aBscXG%l{fUP`*Q!_gcM-|X6%U>n za6H4SdRxkT{PS=4r&+K(;rzT72J5i z%lYTq$-@VmFZ-Lfw+Gb9YD@uTd10#_72GhN#5PZV%&O!uxc3`qO+0k8PK(@T^dYoT zOuGTntG#=}KLra>Ll zsoN$PdV!k|)2~vkh;5^(p&G$c=~pv3o_OA~NJCWSR#m^X@r7oq{%(B20T))@ z8>TK$KoPz0-}xl}6#V`F*(YJ-`0FVc3kM6pC1GOxzdcy@P#(2jW4 z0=e<63$9~8AzXssgrZk~axC|Y^S?Q7EucBpoBKeo+Pk<}-h`3}4vdlrT4Q#r%UYEd zkD^2*=#~fqR**qVl*tS;v>yRsqzMCOO0J~az3?*Zn^@vwtZ%`=0HQg71+BdY=Qj}$ z)MX;n%Pn}AqQ^*Lj~ir{LV`h^=y!a4)Jr^_r>m3t=3mZM`{aMBL%fd+2muzp!Tnbn$Ts-6i%%;lKlK76iRsLe{1~LU$eU(%5&k^k{^?}x z3CX*0b%vQX3!BFt79wDojc@)nKwz9K<$Y=6y_qY9*aPM^8flPwd#9Px%ewStWioPH z%}KO{#C$88S%3#bQZ88(?HllJc=4s2Y`iQDb?y!a1dH2H1t%fZ(N_7de2}F)iXwk^ zQT@L}(qv@dWd4U|%gMm{A3WQWbcJZ6W~=>9Nl$+iD#f3`5x`p~5aJp2k=XksjEMNH zTQ`-ftETrrca1t%30~zbo~=)^#`r|I_fqJNl?eT$3JKzGR9tgft*H2et9qB-4czGQ zSp{H+r4r^bN}uCiWmWp(>xi@_ItX^A$cJr<+M{dF;p`-?vX5oyxIVaoagI-Yj@bJt z7RQ+OJU0J1f-P;&f<_egjI%FnYJfvbBWuStj&UhrTbxNW{gGI3z_j*0K4Pk{o-7*6 zYCKhlfN@FF3D6)eF&}(T*OW>&1QTvzf`?}~@d~i&ix*%O*A1C!+&*L=5#tzCO(WLn zMM@=+L7hPg1$I8Z64`Lp4wpo0_}jI|4}Uc7VdR3S>w0X%bCY%S0`PmHXrg4p^^_fY z-Vdda*Yu-NWVe+Tg!|J3J7I?~Oro$6K&#bzT`lk`cz{oFe6}jrO~(eWsP1 zCcJZn&o?_cj7vki@JyoNRbx|Iaxr}Ib!5@k^;lL-Syho_yn!>RJGLDd`TaQARrnH7 z9uWX9(I zuy+!TY8YR{=3L_XB|QEm#6R^&%g5gzjK-rwdpI-UZ+-92hckLg%f4jKo)b8|VVO>0B_jY}^d0!nw-qAx%VNn?hA2MGi?dyN>YuDAJ5THLU- zVdKEffFFl2@vrUvRdwRv&cGi>FbS>&44@~!tB@K0N`reVQ6yL(nV2(j@p{6Y+ zG&Y1q`DgyDTN84bDa;Lq=e0c06Gp^!TbS%^xV6EH7NrXrju+EN{No@%iL8b*MYU53 zcDZ){3w7Br_H7iY@H5VJtBDeB)8*GrEgqgg?frxP+@t(L%yG~^Nt7@@da6hzTc*4s zW*=M^)%~Vlk)8sOaypzr+LipEPq&Mef$|(BQxWYH-O5TC3fqJD9L?? z%|5`lJr280aj@drPP(}^YAd(nxCx*VzKD9n1EM?C2v31Q)ZI{l8Pp(o0CkbT8$kgm zWE=^9{0}9x!cj&CKLBhNFM?YY|_AhxzP>oVgpQ|=>z$Hha8`Sv(=o1tKoOq6V~P5#@7x{ zGXG%5ne*=L<8F>gHMI#NQ-#t^Oq(MCG^myT16)jcxKYkx~Hy~)0_pa{<)GljiE82P52k{Y zU>WLF15B(*CDqa#4xqu-8Ng!v~LP&Lj#RC51&ET%{MgQJ<5E)vW_&G zThe6I6KzB^o2G;ZHFpWafsCiXuKe`XzyZscV$>XW0ohmZ+=#F>H3dO1=Gml^7PPoU z^khMVvnl^?x4DT2J2w7eH+`Lh5ZWs zpy##S`qCOz)brjTn@H3%hTIVy7mX)Z25!|o$-&BIt>f5bC*J!gfHjWVw&HySAP=i& zR5{(Izi6S9IpcoqayUYU!u%{0M&CJ0dS<1N3M%jb38H#8btqza*F__$ahF3qDjvZ2 z9=nDix2!nVlD5mif%l&(LB|g!l(DGc(1S(}-2ERYgANu>K6N30^A2VEAzIgx&DO_a}nfi|HY!yCs!4Rh|k?{Xuj8Gi&GmHlL z$z!)%!VHcE%i8x6gfxEDDbg8Y_io|i9MNa9u7n+DYCr&`72d%j8GPF)9`FYY@qmGa zIdf5G{O#(qJ_9B053$$29RKRO+n=FF+zOr_O3y`Z@ezlh&$-8AQTLraNZ89i?}J`` zY@05sO>hy20>qU5#oFgJoGt`KJmPd7_LC=6AB zMI^+@v6@3;nzJrW6~A77D<^6Wb?M>BCxcDg=rBk9v?XhS`iZ0nNzMiKQ<`O7sIDeQ z-8g!0Aj=0z`LCJ!J}P4IJBde$Z!T0F%Me&V=j>K&;BYUI~a4Xh$w)2f3XJ zp0CFBm)rBhlnc|9hZ|D6Ma@u%tB!&6KbOl${J>!7m2tb(fg|^Tx3{Q@jG&N-9AR{o z26opvF9PkKH=)@XNh^$WYbZ3MVI!&`N;5vR62`n9IE8Ckl~1-3usz(3YG~YP-yHcN z^Mm-18|OY1yPADOgn?m(QnZvE+?TJ3SPUYF&1|^y#@h{+LFU*@xF+$JT0v3RX)}=z>c%D6CoyPNCyz9l5qtBYI1Fww0_`!z8AZm;cIy+cumKrW zvMBe0C-k7jU$IIENq-pb_(b5zw~dQb7aDp*L`274GBW84&Kz89ijamHWD-Bo_7LIe zdo!hy9$WHG@Kn>M6okflFxGq!z3UrlRvV;nkWGzRJx~;$kz@UQ zx#%DFp(g5)q{J|@Hf|+pb4y+9k$W)7?wTwXxz9(JHX&&yI`)ZCkze#^@Q)q1H7M|k z_+N#Rqv8SBp`4_Q&LmRo3UXMmR>XRSWDyYov9SzHD59c0L*iI~f&H$gX6iR6r-UXY z{ht;_&haixe|DHM*8&I{Po6sCY=#`t-CT|*5f%;^FyV6n-i>6bX9P>saJi=n-7_X=x(#FLh!1a)9_N=EFsDR$#HnM?V zm}`{hj+Ql$r9q5iXcJ>In3&WB8K5iXGKm=y{YuE{^$c131dO#m)awLp%5%y{ z^WwQ(^sAtg8`L^-3BgBX4EX`M-%>TMk=aPWF9WfP(TW;z3TD9Fy&y6xd@_HTzBL?x z6aroy_O%;$0{`ha{wDX8!_MK*bS&hB1Z31tPC4#lPAV{v;QGb3n4o&wx6vjo z2hkj)1wShJfFBqELlF(K#`8hRoi(9isgit^+>wE9q5@_oM=2H=OPI_fAU{fy4?Uxv z(jspu>tjyTEk_|z(Ju-MRNk@_{Kqup$o`>gIQ~<1|8e;IBfU*g7`jS%vHqv+tRcC~ z#t33AVKwB4`ub0dpCw6DBnqfQmC0jE$rKYs%#kiiU zP*JEtt57wIlh2{a9avs0YKSs^mu#W{OnRWegzF)`us_1&u}FSa2;B)_1{S6;XHY0O zLUSnsIfv61q7C7t{H!&cO|4$rgme8=aLwi~2$(Tla52jbkysySFpR_Qw#olyeh`wA z0)YQO1rpv2z4*4wKwVh}(xWVp0t2N4tVc~G4IWVPaQ>?jNNpxaj$3R9>c0>#peYo} zNl6U5ODn&kcuLNk?SEdHAc0Q}nYYzR^Va$&v-rpugY>RtyWAVR@u9)Q#NA0+ zQjGlq2^~@4@_*R6iUr*l1jOvGOtjq|?9;8)d-_%&}>=QsTvS*4(78tR1r z4k%f1%0ukQZ)bX>uS@0*wfhjtcUz!sf>7<%1LJ3)Y!dP&Y|h)k7*6Yyov+2U&`OfGn9a z*Wg1zQ33;7)njY$ZniE&fPp3W_@E%kgMf^{e{SxkMuOU1E7K)T$no_Hls~#fqYVHo zzauS~E64*eRKj2gbqi6dLG8Yk3lO30`1^Ut=cL0pDEV5OYc=3vZhX#;pz4B^--4Fp z5e)r6c?m|Il@oxRz~CoO_+e%c`qoy+Ob-cW>X64(AE^WS*HrVsVAe#SYY?dysTUbQ ze@>bI^k7`O{-SOhG~oSD*}(hI`{2FmA~N`%6imOIn2EGVI%2+%4gaB1@`W_24Q6nU z!i^ayq)8$q1B)h~8YFoP5NgqjTQD8D-wa3^XJrd&=_GLJE*AK4J;VG{0&){hsY+D?$maFRKSE_kvqH{^oSISqt+S2-K5&t zryRPWV6hN!Dc}_)Fz7&{TqJ%ppGm)WdZ1izA_t`U^o=&e)+Q1v z5ne{9;NO1QNi~o=)P^B&25s#?k2R13@&==y6n&Qgm`emuG_pJ+V9$Yiq25|II$43Us{C|eX_SrUp8OB3QZ+P>_-2YaOYlFczM$Wk&eMnBynBmdPdRL5jv3y@r17aNPI`$*AcLwiexiz zB0V6}8LiK6Ap?>L(U1a;JRmm!Dp7u$c7_9%bsWS&(nl70 zk}7vVy*LqrK^Y|Ik2)|HI-o}#GLZnD5TruX9tWWmS*U4fJucvp-zz|`!Nq9-o>m8? zJ0_5s(4sq_XAfj(G!kE-7zU`%NY|{4I;elKjhldtBBU2IA~g^&DIeWH*ilH50RcE7 zHNgwt5WxioC_W@c2z{Ux7?APe14{IM4~0Qupy2>61Yk>w8CxhHmfR>|pbBV5JfS72 z;%y|7N})cLkR}*F9Y0upkeKM~SJT7~T`zzopedjUlY`J>P|OA@N}y1lyf_+6s1UIN zCNQqXT+hrVlO}QJ|164-`j!SyUy6EufbJYCTurElL4Ae;?)+N#baE z7BVz==ug5zQ-Fu%NKD8=6hcEHP^nPFsKkaOU>5~Gsc0Xyb{9aXsRYQu)e7a+$YxNX zZ$*T7W93E35|E)&x`^O<{xeQp2%3coJyg&>M=tn$$j0;5H>d^yow-LRsQNsL-w?#ByXi zp#eM!NFGG`SpQ0M1K-(Z@}a+(3u&dx&r{6QLpxdttw|1PK)ccalI($HRI>%uGnPl{ zS;1L=o(T!)g8`fTQ8Q4(?Onq9_4^I8K%TT1F;qoMz>&82CwTyP#&;i7jE!y~0-2$F zXjc~EXo{VwfZvTbsz6>;d^%w+RDpA7@EN)EZ%{zPXR5{V|6*k40XtfNE1SfTR&nqT z7JGm@~8set-~23RXf9n3m4x z8~FD+pd^g0pCD>q#Ia$i42X?rH{8KJz9QXB9rYeqBH7gor54HWQ6;#-7-fM7X$Y9Y z>Jk(8(s1`azrI$T8=6p`cecKtZ8idK>+b!&L7cw+dYBlKh*lFP^jT5d9!CBH@DZ6n zCRlMikRu9GZw+$qdba=T>T=^bU&q_S-s zmG}GREn(T?=IV2&_iNVPyT@~@%l+nC^5Z_?TKn_E%+p{ap!wZqtGD;-o9|lZ)20ER zJKk<S{c1eEZ&8Wx{*54K4a5GbODzRm*Y+FQQ++j@OqNiKp zJTi_*hk0K)@hsxDXNAZk`N$$Gq|&bG&8%+)ljpNp7{3)jzf;KIkek&wO2pgAq`kQR z?81tJ2#RG+bju$%Z4|!#H(vHme0Oh+tfNJA)5+jl6 zXc?E|%K@3L>F|9>+3EDWQ~~z$BlQ#Ioxs zp~RQUlGrctbvCx3q!YnS0VRut^-$s*W$$Ip>VRi(HSCa+zJG}G?6bjyZR0=`o^#pB zknPkWo-@tqkS$^ToxYaPCxrE5CdDk?ucW(+*KGd|l(IZ0QmH07j>Zy9)}4$;YCrNY z53V|ArEZ^DOpMvy|1jBWdcI<|U-5iTR>P|JPmF|7Sw((Mj^Q_@rEUGoyhjeh&&1I)agFhP2`S{d!{Q8V=+j?xE zzoqXED!m<(0>?kX_#{zKk@;?fC=;7U)QH93mF2r=T%;AfP==``H;SF80d9)ggTzm^ z+=HnF3aTVNoaN8`OJWAN)oL%Mmd6-ngz#h9qXS7qkwnEeeGJkuhoNejK0j;y^gyI1 zsDq#jSZJ^*zW*#YP9aOI&C`ohlV#`w#LxB#k?`XbM2;u3|I8tc@L4wWXTQAvW z#w@}F4izxwF-sWkm{+xFXdM#&U8#}9%qgvd5zp%67d8%PGF->=A<Jt#OEKPMrO4Nk299ad(5~y zwTgZtN#DQ{A>^Q`(X&u^th+`N!~3E9$45SmQdi(g86+-6eyf%|ANaZ8<|k&D?*q3; z86d$1V}ugvM_X?R9)b3c0v9Pm^jKiP(opaMLB}9mx)U~R zlQqjV3EGb|`D%;;B(BKVXjl%1M01&IfsYg6CnLnzV6>jvh!=s9%N8kZtk{4S3c&)W zc*!XXELng^ZWDkKwB(=#MDMoMdlf;a3gdEHS9^8#8B|k4uv$2uUrluOZsiWGQZDw3> z!U>2iyhR|+^%~#>tr}oI?OI@=4inI>79-GtHWLH6A#v!*zF5HSEKWeEJtY8vs5n4A#|Gp;gWxuv|ajiJ9=coMuhJsEC-qA45Y z>cL{MdAQ|9v@2T!7!7$bb##J2Wra}-fmTzlp;U8!VQ`+N!L*}-dDPD$*Ex<;x?Lt- zT(luIHp;k5OL8o}zZF$ilzN%?xFBCv$J0Bw`kO1^=q zU?iZ(&(A3Om0C$cQ(Bl)0na+yNeY0gND2T_K!M4Ut4MCB$RtLV_KrZPs89*I4pRZ2 z)dE)N0ahF{BRH6bSJ~xiKv+`}#aqgs7cS-yRhamfGL1qc8fC%5Z2nwUQ_)IpRFVAo znT=A5yGAfjQ_@D6N|TK;SPJ^ic;q3Ohx3^Vc+E==+UDL}(x;Pfgn0KN*iPK_B^OVT z-kUOb360~Upn6+u!)cSoi5}A?_Rf6@6XoT$mibI z$@hr&ollSap3gtnu_X!ww%+DDJC_lzUY+mP`Cb6i>fpvs&jn58a4B!!(HD|r)p!0m zk+#=OL~oau{D&RPXzkMYrh}b%z%sMxz(JGusV*ze>BKRxzu&^`^&q}9eCcf75eu_Q z%X_q_9@~h<>Y}eNRGpYND`wF+x7g1uVWI6^BG$~v zRP=8k$#B^S(u|_WkbItOt=h~wE?Kp|y>{xCQ{`urKSk5cs=rXnB&ATGtL}lmh#?b3(XWD#N z4sGt`ITlfQpd4%T+~G9T^b(SL9~A7uf}S3%PLaQY?bJumUwgzBftdeMQ~e2dp+ zE#IfM``L=zQpJx1TBG+ayU%a4^NO>row##Sg~uvNpdA?HZz+DW86{QdW$#)vpA8&4lghp5)K^O{>Wy!|wivUr zZdgnm&F2Ar^j=^3nK{XOp1?viJFZe?;OThK^gVOsB$wU&CY|@h7_oEFVC97I>zx|A zJ6@y7iJ0`!se0n_GdE zuu+FR`Lw0a3A!aS@zcJ6caL+h=F*$D;cS0*4>6G8BehvaaYCSyS$G~3M*2vN12t}A zzTfj%*~W8NnPsCnwVq>ffulUtaqiE#;E@HgL?dQpHi!Z{OIyPg1KYjr2F`2f<&4Jy zt8tGJ1d{UyBEd6@Qp_W5YYtY7b(Fwe6ouEgS6%sYKW{Z z<#`a%j=)H-g$i8;n5-@*GVlb0CEeQ$yZEP;<}PiB_I*PAc-0DwVt9CvdtFVNW3f?T zoJ_QSoJ;`O77)2Tkr0_Z8BZK13PFGxbhLg{fEXhrpD43fcYbRA#?+X?1=b#=3!Hrp zc@QTILHO@-(PY3V)@{HHN#6_F_N5m92(0k-OkIAJ40q)-*KtT-1^80+#+LIQUY1!G z>M&n+*zMevQtn=C4 z*gOw$T?Px zc^j>1co}wWj@mS9)0W)BphDb#fb=wL^OnaRIlsc+sN}Hl5Af|UsZb-$y8R-&>TkUiRO;`}TK9X8tSX`4922BNAk+aT(G4tMYKw&gFiB`jx{v7}NzHyndArlV~-a zWWd!Bw(lF9NUPQ~8PjWa6WMg1s6M}K(2Mum_LqcPR<>(WDA|-Sg6CJhgUYx2j*oug zUg@_!U1eu!sBjr8LoUjhh*w`cQ(Rax07LCw{%KcNfSQ+0Y=9u0xiPXgb$M`e41e2Z zZ+t2PyOqu4j5UqFFQPHnaNSadP7l_Eq<0Tji;Tz5;BuKLT2&7q7He0WY! zgxbt_zXUwCE9?yJSXpZOSNWg_N%J8}hCvv@V{3WmmBe}dA+?QYbf$(&1U08C1j7^0 z^Colzc1OdEHfIj{iRRd-2sZPkif|mJC;8rW?#!iu(qjDbv@hEn53};KR$6p$Wrcga zyv5+3!*LMgrOi16k!NC%({Y&W6oo}8SPjWEKten9dJwfwfRVIMVu#dDe4jcNK;Iia z-uW!DyOS}o{aFy)H=rK$5G^-?d10`B5CldTM~@)K^V-nr*FdsG4ih+LgWpl|zzq-t z7IiOxijimVBB6~Fj8^MMqm%maVlesf+IaXyD-!cuz17#p#9|%n?q1?2NAL3nL_$6d zZa>1-XO3n^?;jth8<~yHCFRr6(ec&y*Y($v*OAwMns$_#XVsc__Y7qpYiYX=I;W_|2KNW^dF6mlr%1ebUoe7p#kOqVV0Yd>qhr; zH{uy1a)`^V$ivZvKa{k-=B*_`KRWExCJb4TJ-GSK7EA%z39(RQ*i@lQ#S|Xvks|J$ zAl<%Ou_ulIOLPI-GJ8Gmy)ZnhlcI1WZ}Obd!Ppd+{5Bfv3`aVSxn@xiPGj+=?IDv* zJEGKps0g7g(iG$wQ3jt)j?I&}mxeAWkyhWU1{T=67V=Fn4G+Kz;XI)U; zRCLo5X{N3kX(rPdN+tm&?b^Y$^>r_`cF&ya$+PHJkLL$?ir~$v#^|r>Db zQDaR^y@Z)tT;vXtM`-YKtm^ZoTD6Yw^Q?cPKfg5UxBk5Fub#i9Uf0XNFT*jB8j6lY z#9>Pr*NrVOZ4X71{VxXSK>c47@~=Pi*!(E?f2ii4_D}m^aQ2@qp;p@2Plx2fR+iP< z9)W29dY-i(2G+LoNyghChjjT@=;o75u<;GjNPXNweBNaiP*8~jAO!s5AHS)4ImB3x1lIT|Ac|YJs?;5nYzM5 zYj!=r_!uFA>m(`bFBZm)0Q>0wxQ4qp^h*S4;deo(=@(EVW>6xysG)6s{FlH+bi)$O28b@gLGw z@~ib`M$YH2J|XC=MAf+v6lXIiDoVZI5bL!u1mOv$ew!WwSJ0DIwKcHi4F!O6rmW~M z=F3U_a)bZ9+4`yC$)CUUmD7CLN@5K-y8V1N(&lXP^n%0n+(t8NRF|7)FKRt1``0?o z`&@2<^WNMJ1@wCVBY9lBU~XN~QVoFhnU%lZJ(~dm^b35I8-$AXyW@>mKwpOP{$f`^ zAfcGpWIWU*-T5gf$!i#dm+h&ba?9zQNK5>iT>wYziwCn<&o%>Oln)`mt;756+HJ7? z!f^H*YJI(Uv&AX-XRTd$Sm*^n46tzPa*3+}sF!d%b$Q-_m>B-+etj&Hv^gb(~b85oL|NxD{>9xVqIs?hyVz z!~O9;{HouB{6BeGf4WtF`B#5}KfJ0xNBv(O*O@cz+ifikcgKXwPDB+xJ-3ik!3pK! z)GybLlv@<_8CY3nmgV!bri?$U%pA*?X-k=2)?aq-y*qUlVEX>Dfxp0S9gZaQK=~&l z_?OPjI9x;OX#R^i{8Q)Nr0%l;ALJ687FhkAvix0G$^SxG*a6Yt{?}Y45s8AhIy#cJYj+dPWKc;LJt`2{A7C3ysE`}yVj zHu`PV=MvkZONgNnNds60Ql#;CKe5<~SP1r#1`-l~9w;c`@E(chi>(7aYj=G0<2{%X z!{IfS$R7+~aodO;5Kdrm(*@Mr3bfYfPG8>A2Ij0N#eN#l{86?6c$;#ty*?jrxaxii z+|E`pY9%9qRBlHeVa*z0x41iWjMK=_E>+34r=FHiK;kXq0W47wgBOO&pTN-pg0}@w zaPHv-NQ^(_4APq2q)6fLUWn&IKFQ7W#W@j96Gsx1eVf|mi_175&Pcy}1lNuvY;I(6 z!^=&-;Q(y~BXdkyfjjxThBKXCIbZ7w1pI=i$S9`3QT&sBHSL!dc#r^EI|sn%Wtg!T615$

f1+5&Kg- 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 P4~xsE_3UY7XHEM*{&Mle literal 0 HcmV?d00001 diff --git a/doc/devicetree-specification-v0.3.pdf b/doc/devicetree-specification-v0.3.pdf new file mode 100644 index 0000000000000000000000000000000000000000..16ed91e78dd93ca275ef6074416e7ae7c307c9a0 GIT binary patch literal 423556 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

1>whloy3)~h#N|ZtU#lbf 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ɤkvkMd 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%0Az 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 z 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 Date: Fri, 13 May 2022 01:05:05 +0300 Subject: [PATCH 025/107] wip: Add wrapper for fdt-rs crate, bootup with DTB This is to easily add necessary functions without touching the original crate - this will probably need to be upstreamed eventually! - Implement DTB path traversal tests for PathSplit - implemented DeviceTree::get_prop_by_path() - Add iterator for memory reg property - wip: impl custom eret to pass dtb address in boot.rs (commented out yet) - calc #address-cells and #size-cells internally - Fix property value reading code (with read_first/read_second) - convert reserved memory to native-endian - fdt 0.4.5 breaks api, pin to 0.4.4 --- kernel/init_thread/Cargo.toml | 11 + kernel/init_thread/src/device_tree.rs | 347 ++++++++++++++++++ kernel/init_thread/src/main.rs | 98 ++++- libs/boot/src/arch/aarch64/boot.rs | 46 ++- .../device_driver/bcm/mini_uart.rs | 2 +- 5 files changed, 479 insertions(+), 25 deletions(-) create mode 100644 kernel/init_thread/src/device_tree.rs diff --git a/kernel/init_thread/Cargo.toml b/kernel/init_thread/Cargo.toml index b25f2fa7f..bab259243 100644 --- a/kernel/init_thread/Cargo.toml +++ b/kernel/init_thread/Cargo.toml @@ -49,6 +49,17 @@ static_assertions = { workspace = true } tock-registers = { workspace = true } usize_conversions = { workspace = true } ux = { workspace = true } +#enum_dispatch = "0.3" +#tap = "1.0" -- check no_std? +shrinkwraprs = { version = "0.3", default-features = false } +# dtb = "0.2" # Dec 2019 by Simon Prikhodko +# Alternatives to dtb to look at: +#device_tree = "1.1.0" # Jun 2016 by Marc Brinkmann +#fdt-rs = "0.4.2" # Aug 2020 by Sean Wilson +# fdt-rs latest 0.4.3 Jul 2021 by Sean Wilson -- interface is more basic than in dtb +# @todo Added from_raw_pointer() finally +fdt-rs = { version = "=0.4.4", default-features = false } +num = { version = "0.4", default-features = false } [dev-dependencies] libexception = { workspace = true, features = ["test_build"] } diff --git a/kernel/init_thread/src/device_tree.rs b/kernel/init_thread/src/device_tree.rs new file mode 100644 index 000000000..9ceae1594 --- /dev/null +++ b/kernel/init_thread/src/device_tree.rs @@ -0,0 +1,347 @@ +#![allow(dead_code)] + +use { + core::alloc::Layout, + fdt_rs::{ + base::DevTree, + error::DevTreeError, + index::{DevTreeIndex, DevTreeIndexNode, DevTreeIndexProp}, + prelude::PropReader, + }, + shrinkwraprs::Shrinkwrap, +}; + +fn get_size_cell_tree_value<'a, 'i: 'a, 'dt: 'i>( + node: DevTreeIndexNode<'a, 'i, 'dt>, + name: &str, +) -> u32 { + const DEFAULT: u32 = 1; + + let res: Result<_, DevTreeError> = node.props().try_find(|prop| Ok(prop.name()? == name)); + + if !res.is_err() { + if let Some(res) = res.unwrap() { + return res.u32(0).unwrap_or(DEFAULT); + } + } + + while let Some(node) = node.parent() { + let res: Result<_, DevTreeError> = node.props().try_find(|prop| Ok(prop.name()? == name)); + + if res.is_err() { + // @todo abort on error? because it's not a None, but an actual read error.. + continue; + } + + if let Some(res) = res.unwrap() { + return res.u32(0).unwrap_or(DEFAULT); + } + } + + DEFAULT +} + +pub fn get_address_cells<'a, 'i: 'a, 'dt: 'i>(node: DevTreeIndexNode<'a, 'i, 'dt>) -> u32 { + get_size_cell_tree_value(node, "#address-cells") +} + +pub fn get_size_cells<'a, 'i: 'a, 'dt: 'i>(node: DevTreeIndexNode<'a, 'i, 'dt>) -> u32 { + get_size_cell_tree_value(node, "#size-cells") +} + +/// Uses DevTreeIndex implementation for simpler navigation. +/// This requires allocation of a single buffer, which is done at boot time via bump allocator. +/// This means we can only parse the tree after bump allocator is initialized. +#[derive(Shrinkwrap)] +pub struct DeviceTree<'a>(DevTreeIndex<'a, 'a>); + +impl<'a> DeviceTree<'a> { + pub fn layout(tree: DevTree<'a>) -> Result { + DevTreeIndex::get_layout(&tree) + } + + pub fn new(tree: DevTree<'a>, raw_slice: &'a mut [u8]) -> Result { + Ok(Self(DevTreeIndex::new(tree, raw_slice)?)) + } + + // @todo drop all the wrapper shenanigans and just export this one fn + /// Iterate path separated by / starting from the root "/" and find props one by one. + pub fn get_prop_by_path(&self, path: &str) -> Result { + let mut path = PathSplit::new(path); + let mut node_iter = self.0.root().children(); + let mut node: Option = Some(self.0.root()); + if path.component().is_empty() { + // Root "/" + path.move_next(); + } + while !path.is_finished() { + let res: Result<_, DevTreeError> = + node_iter.try_find(|node| Ok(node.name()? == path.component())); + node = res?; + if node.is_none() { + return Err(DevTreeError::InvalidParameter("Invalid path")); // @todo + } + node_iter = node.as_ref().unwrap().children(); + path.move_next(); + } + assert!(path.is_finished()); // tbd + assert!(node.is_some()); + let mut prop_iter = node.unwrap().props(); + let res: Result<_, DevTreeError> = + prop_iter.try_find(|prop| Ok(prop.name()? == path.component())); + let prop = res?; + if prop.is_none() { + return Err(DevTreeError::InvalidParameter("Invalid path")); // @todo + } + Ok(prop.unwrap()) + } +} + +/// Augment DevTreeIndexProp with a set of pairs accessor. +#[derive(Shrinkwrap)] +pub struct DeviceTreeProp<'a, 'i: 'a, 'dt: 'i>(DevTreeIndexProp<'a, 'i, 'dt>); + +impl<'a, 'i: 'a, 'dt: 'i> DeviceTreeProp<'a, 'i, 'dt> { + pub fn new(source: DevTreeIndexProp<'a, 'i, 'dt>) -> Self { + Self(source) + } + + pub fn payload_pairs_iter(&'a self) -> PayloadPairsIter<'a, 'i, 'dt> { + let address_cells = get_address_cells(self.node()); + let size_cells = get_size_cells(self.node()); + + // @todo boot this on 8Gb RasPi, because I'm not sure how it allocates memory regions there. + libqemu::semi_println!( + "Address cells: {}, size cells {}", + address_cells, + size_cells + ); + + PayloadPairsIter::new(&self.0, address_cells, size_cells) + } +} + +pub struct PayloadPairsIter<'a, 'i: 'a, 'dt: 'i> { + prop: &'a DevTreeIndexProp<'a, 'i, 'dt>, + total: usize, + offset: usize, + address_cells: u32, + size_cells: u32, +} + +impl<'a, 'i: 'a, 'dt: 'i> PayloadPairsIter<'a, 'i, 'dt> { + pub fn new( + prop: &'a DevTreeIndexProp<'a, 'i, 'dt>, + address_cells: u32, + size_cells: u32, + ) -> Self { + Self { + prop, + total: prop.length(), + offset: 0usize, + address_cells, + size_cells, + } + } + + // @todo get rid of unwrap()s here + fn prop_u32(&mut self, index: usize) -> u64 { + self.prop.u32(index).unwrap().into() + } + + // @todo get rid of unwrap()s here + fn prop_u64(&mut self, index: usize) -> u64 { + u64::from(self.prop.u32(index).unwrap()) << 32 + | u64::from(self.prop.u32(index + 1).unwrap()) + } + + fn read_pair( + &mut self, + size: usize, + read_first: impl Fn(&mut Self, usize) -> u64, + first_index: usize, + read_second: impl Fn(&mut Self, usize) -> u64, + second_index: usize, + ) -> Option<(u64, u64)> { + if self.offset + size > self.total { + return None; + } + let result: (u64, u64) = ( + read_first(self, first_index), + read_second(self, second_index), + ); + self.offset += size; + Some(result) + } +} + +impl<'a, 'i: 'a, 'dt: 'i> Iterator for PayloadPairsIter<'a, 'i, 'dt> { + /// Return a pair of (address, size) values on each iteration. + type Item = (u64, u64); + + fn next(&mut self) -> Option { + if self.offset >= self.total { + // @todo check for sufficient space for the following read or the reads below may fail! + return None; + } + const STEP: usize = size_of::(); + match (self.address_cells, self.size_cells) { + (1, 1) => { + const SIZE: usize = 8; + self.read_pair( + SIZE, + Self::prop_u32, + self.offset / STEP, + Self::prop_u32, + self.offset / STEP + 1, + ) + } + (1, 2) => { + const SIZE: usize = 12; + self.read_pair( + SIZE, + Self::prop_u32, + self.offset / STEP, + Self::prop_u64, + self.offset / STEP + 1, + ) + } + (2, 1) => { + const SIZE: usize = 12; + self.read_pair( + SIZE, + Self::prop_u64, + self.offset / STEP, + Self::prop_u32, + self.offset / STEP + 2, + ) + } + (2, 2) => { + const SIZE: usize = 16; + self.read_pair( + SIZE, + Self::prop_u64, + self.offset / STEP, + Self::prop_u64, + self.offset / STEP + 2, + ) + } + _ => panic!("oooops"), + } + } +} + +// See "2.2.3 Path Names" in DTSpec v0.3 +// This is based on https://lib.rs/dtb implementation (c) Simon Prykhodko, MIT license. +struct PathSplit<'a> { + path: &'a str, + path_component: &'a str, + index: usize, + total: usize, +} + +impl<'a> PathSplit<'a> { + pub fn new(path: &'a str) -> PathSplit<'a> { + let path = if let Some(p) = path.strip_suffix('/') { + p + } else { + path + }; + let mut split = PathSplit { + path, + path_component: "", + index: 0, + total: path.split('/').count(), + }; + split.update(); + split + } + + fn update(&mut self) { + for (i, comp) in self.path.split('/').enumerate() { + if i == self.index { + self.path_component = comp; + return; + } + } + } + + pub fn component(&self) -> &'a str { + self.path_component + } + + pub fn level(&self) -> usize { + self.index + } + + pub fn is_finished(&self) -> bool { + self.index >= self.total - 1 + } + + pub fn move_prev(&mut self) -> bool { + if self.index > 0 { + self.index -= 1; + self.update(); + return true; + } + false + } + + pub fn move_next(&mut self) -> bool { + if self.index < self.total - 1 { + self.index += 1; + self.update(); + return true; + } + false + } +} + +#[cfg(test)] +mod tests { + use super::PathSplit; + + #[test_case] + fn test_single_level_path_split() { + let mut path = PathSplit::new("/#address-cells"); + assert!(!path.is_finished()); + assert_eq!(path.level(), 0); + assert_eq!(path.component(), ""); + + assert_eq!(path.move_next(), true); + + assert!(path.is_finished()); + assert_eq!(path.level(), 1); + assert_eq!(path.component(), "#address-cells"); + + assert_eq!(path.move_next(), false); + } + + #[test_case] + fn test_multiple_level_path_split() { + let mut path = PathSplit::new("/some/_other/#address-cells"); + assert!(!path.is_finished()); + assert_eq!(path.level(), 0); + assert_eq!(path.component(), ""); + + assert_eq!(path.move_next(), true); + + assert!(!path.is_finished()); + assert_eq!(path.level(), 1); + assert_eq!(path.component(), "some"); + + assert_eq!(path.move_next(), true); + + assert!(!path.is_finished()); + assert_eq!(path.level(), 2); + assert_eq!(path.component(), "_other"); + + assert_eq!(path.move_next(), true); + + assert!(path.is_finished()); + assert_eq!(path.level(), 3); + assert_eq!(path.component(), "#address-cells"); + + assert_eq!(path.move_next(), false); + } +} diff --git a/kernel/init_thread/src/main.rs b/kernel/init_thread/src/main.rs index 77a28e881..6c9ff9fa2 100644 --- a/kernel/init_thread/src/main.rs +++ b/kernel/init_thread/src/main.rs @@ -2,6 +2,7 @@ #![no_main] #![allow(unused)] #![feature(format_args_nl)] +#![feature(try_find)] // For DeviceTree iterators // Init-thread process. // - Start initializing the kernel @@ -34,6 +35,7 @@ // - scheduler (invokes process upcall key) mod boot; +mod device_tree; mod el_switch; mod embed; mod loader; @@ -42,7 +44,12 @@ mod paging; mod syscall_test; use { - core::{panic::PanicInfo, ptr::write_bytes}, + core::{alloc::Allocator, panic::PanicInfo, ptr::write_bytes}, + device_tree::{DeviceTree, DeviceTreeProp}, + fdt_rs::{ + base::DevTree, + prelude::{FallibleIterator, PropReader}, + }, libcpu::endless_sleep, libqemu::semi_println, memory::{BootAllocator, PhysAddr}, @@ -61,6 +68,12 @@ fn panic(info: &PanicInfo) -> ! { endless_sleep() } +fn dump_memory_map() { + // Output the memory map as we could derive from FDT and information about our loaded image + // Use it to imagine how the memmap would look like in the end. + arch::memory::print_layout(); +} + #[unsafe(no_mangle)] pub extern "C" fn init_main(dtb_ptr: *const u8) -> ! { semi_println!("init_main started"); @@ -84,12 +97,85 @@ pub extern "C" fn init_main(dtb_ptr: *const u8) -> ! { semi_println!("Parsing device tree..."); - // Next step: parse DTB! - // let dtb = unsafe { - // // Direct physical access - MMU off - // DeviceTree::from_phys(PhysAddr::new(dtb_phys)).expect("Invalid DTB") - // }; + // Safety: we got the address from the bootloader, if it lied - well, we're screwed! + let device_tree = unsafe { + DevTree::from_raw_pointer(dtb_ptr as *const _).expect("DeviceTree failed to read") + }; + + let layout = DeviceTree::layout(device_tree).expect("Couldn't calculate DeviceTree index"); + + let block = allocator + .alloc(layout.size) + .expect("Couldn't allocate DeviceTree index"); + + let device_tree = + DeviceTree::new(device_tree, block).expect("Couldn't initialize indexed DeviceTree"); + + let board = device_tree.get_prop_by_path("/model").unwrap().str(); + if let Ok(board_name) = board { + semi_println!("Running on {board_name}"); + } + + // To init memory allocation we need to parse memory regions from dtb and add the regions to + // available memory regions list. Then initial BootRegionAllocator will get memory from these + // regions and record their usage into some OTHER structures, removing these allocations from + // the free regions list. + // memory allocation is described by reg attribute of /memory block. + // /#address-cells and /#size-cells specify the sizes of address and size attributes in reg. + // To get memory size from DTB: + // 1. Find nodes with unit-names `/memory` + // 2. From those read reg entries, using `/#address-cells` and `/#size-cells` as units + // 3. Union of all these reg entries will be the available memory. Enter it as mem-regions. + + let res: Result<_, DevTreeError> = device_tree + .props() + .try_find(|p| Ok(p.name()? == "device_type" && p.str()? == "memory")); + let mem_prop = res.unwrap().expect("Unable to find memory node."); + let _mem_node = mem_prop.node(); + // let parent_node = mem_node.parent_node(); + + let reg_prop = device_tree + .get_prop_by_path("/memory@0/reg") + .expect("Unable to figure out memory-reg"); + + semi_println!( + "Found memnode with reg prop: name {:?}, size {}", + reg_prop.name(), + reg_prop.length() + ); + let reg_prop = DeviceTreeProp::new(reg_prop); + + for (mem_addr, mem_size) in reg_prop.payload_pairs_iter() { + semi_println!("Memory: {} KiB at offset {}", mem_size / 1024, mem_addr); + } + + // 4. List unusable memory, and remove it from the memory regions for the allocator. + for entry in device_tree.fdt().reserved_entries() { + let size: u64 = entry.size.into(); + let address: u64 = entry.address.into(); + semi_println!("Reserved memory: {size:?} bytes at {address:?}"); + } + + // 5. Also list memreserve entries, and remove then from allocator regions? + // From FDT dump: + // memreserve = <0x3b400000 0x04c00000 >; + + // Iterate compatible nodes (example): + // for entry in device_tree.compatible_nodes("arm,pl011") { + // semi_println!("reserved: {:?} (bytes at ?)", entry.name()/*, entry.address*/); + // } + + // 6. Also, remove the DTB memory region + index + semi_println!( + "DTB region: {} bytes at {:x}", + device_tree.fdt().totalsize(), + dtb + ); + + dump_memory_map(); + + // Next step: parse DTB! // unsafe { // BOOT_INFO.dtb_size = dtb.total_size(); diff --git a/libs/boot/src/arch/aarch64/boot.rs b/libs/boot/src/arch/aarch64/boot.rs index ede8420f7..743b564f8 100644 --- a/libs/boot/src/arch/aarch64/boot.rs +++ b/libs/boot/src/arch/aarch64/boot.rs @@ -8,6 +8,9 @@ //! Low-level boot of the ARMv8-A processor. //! +//! Raspi kernel boot helper: https://github.com/raspberrypi/tools/blob/master/armstubs/armstub8.S +//! In particular, see dtb_ptr32 + use { aarch64_cpu::registers::*, core::arch::global_asm, @@ -24,11 +27,11 @@ macro_rules! entry { /// Only type-checks! #[unsafe(export_name = "main")] #[inline(always)] - pub unsafe fn __main() -> ! { + pub unsafe fn __main(dtb: u32) -> ! { // type check the given path - let f: unsafe fn() -> ! = $path; + let f: unsafe fn(u32) -> ! = $path; - unsafe { f() } + unsafe { f(dtb) } } }; } @@ -55,7 +58,10 @@ global_asm!( /// We assume that no statics are accessed before transition to this fn. #[unsafe(no_mangle)] #[unsafe(link_section = ".text.boot")] -pub unsafe extern "C" fn _startup_in_rust() -> ! { +pub unsafe extern "C" fn _startup_in_rust(dtb: u32) -> ! { + // On entry, w0 should contain the dtb address. + // For non-primary cores it contains 0. + // Can't match values with dots in match, so use intermediate consts. #[cfg(feature = "qemu")] const EL3: u64 = CurrentEL::EL::EL3.value; @@ -66,9 +72,9 @@ pub unsafe extern "C" fn _startup_in_rust() -> ! { match CurrentEL.get() { #[cfg(feature = "qemu")] - EL3 => setup_and_enter_el2_from_el3(), - EL2 => setup_and_enter_el2_from_el2(), - EL1 => reset(), // Cannot configure memory mappings here properly, fail instead! + EL3 => setup_and_enter_el2_from_el3(dtb), + EL2 => setup_and_enter_el2_from_el2(dtb), + EL1 => reset(dtb), // Cannot configure memory mappings here properly, fail instead! // if not core0 or not EL3/EL2, infinitely wait for events _ => endless_sleep(), } @@ -109,7 +115,7 @@ fn shared_setup_and_enter_pre() { // #[unsafe(link_section = ".text.boot")] // #[inline] -// fn shared_setup_and_enter_post() -> ! { +// fn shared_setup_and_enter_post(dtb: u32) -> ! { // unsafe extern "Rust" { // // Stack top // static __STACK_TOP: UnsafeCell<()>; @@ -122,7 +128,11 @@ fn shared_setup_and_enter_pre() { // } // // Use `eret` to "return" to EL2. This will result in execution of // // `reset()` in EL2. -// asm::eret() +// // Load DTB address into w0 prior to eret. +// unsafe { +// core::arch::asm!("eret", in("w0") dtb); +// core::hint::unreachable_unchecked() +// } // } // FIXME: This will be called by init_thread later. @@ -131,7 +141,7 @@ fn shared_setup_and_enter_pre() { /// Prepare and execute transition from EL2 to EL1. // #[unsafe(link_section = ".text.boot")] // #[inline] -// fn setup_and_enter_el1_from_el2() -> ! { +// fn setup_and_enter_el1_from_el2(dtb: u32) -> ! { // // Set Saved Program Status Register (EL2) // // Set up a simulated exception return. // // @@ -147,13 +157,13 @@ fn shared_setup_and_enter_pre() { // // Make the Exception Link Register (EL2) point to reset(). // #[allow(clippy::fn_to_numeric_cast_any)] // ELR_EL2.set(reset as *const () as u64); -// shared_setup_and_enter_post() +// shared_setup_and_enter_post(dtb) // } #[unsafe(link_section = ".text.boot")] #[inline] -fn setup_and_enter_el2_from_el2() -> ! { - reset(); +fn setup_and_enter_el2_from_el2(dtb: u32) -> ! { + reset(dtb); } /// QEMU boot-up sequence. @@ -169,7 +179,7 @@ fn setup_and_enter_el2_from_el2() -> ! { #[cfg(feature = "qemu")] #[unsafe(link_section = ".text.boot")] #[inline] -fn setup_and_enter_el2_from_el3() -> ! { +fn setup_and_enter_el2_from_el3(dtb: u32) -> ! { // Set Secure Configuration Register (EL3) SCR_EL3.write(SCR_EL3::RW::NextELIsAarch64 + SCR_EL3::NS::NonSecure); @@ -189,16 +199,16 @@ fn setup_and_enter_el2_from_el3() -> ! { // Make the Exception Link Register (EL3) point to reset(). ELR_EL3.set(reset as *const () as u64); - shared_setup_and_enter_post() + shared_setup_and_enter_post(dtb) } // Enter Rust code in EL2. #[unsafe(link_section = ".text.boot")] -fn reset() -> ! { +fn reset(dtb: u32) -> ! { unsafe extern "Rust" { - fn main() -> !; + fn main(dtb: u32) -> !; } // SAFETY: We're getting to more safety right here! - unsafe { main() } + unsafe { main(dtb) } } diff --git a/libs/platform/src/platform/raspberrypi/device_driver/bcm/mini_uart.rs b/libs/platform/src/platform/raspberrypi/device_driver/bcm/mini_uart.rs index 1ad9f227a..5ecfd58e4 100644 --- a/libs/platform/src/platform/raspberrypi/device_driver/bcm/mini_uart.rs +++ b/libs/platform/src/platform/raspberrypi/device_driver/bcm/mini_uart.rs @@ -170,7 +170,7 @@ impl From for u32 { } // [temporary] Used in mmu.rs to set up local paging -pub const UART1_BASE: usize = BcmHost::get_peripheral_address() + 0x21_5000; +pub const UART1_BASE: usize = BcmHost::get_peripheral_address() + 0x21_5000; // DTB says 0x7e215040 impl libdriver::drivers::interface::DeviceDriver for MiniUart { type IRQNumberType = IRQNumber; From f4ac2b68f7f0e259671c29e73cd1b32c4b97b631 Mon Sep 17 00:00:00 2001 From: Berkus Decker Date: Sat, 12 Feb 2022 02:34:42 +0200 Subject: [PATCH 026/107] wip: Add more dtbs for the boards I have (from explore/dtb branch) --- targets/bcm2708-rpi-zero-w.dtb | Bin 0 -> 26133 bytes targets/bcm2710-rpi-3-b-plus.dtb | Bin 27082 -> 28599 bytes 2 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 targets/bcm2708-rpi-zero-w.dtb diff --git a/targets/bcm2708-rpi-zero-w.dtb b/targets/bcm2708-rpi-zero-w.dtb new file mode 100644 index 0000000000000000000000000000000000000000..0b63b9e9c12bc3b239f7c725e66542a261380961 GIT binary patch literal 26133 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 ASO5S3 literal 0 HcmV?d00001 diff --git a/targets/bcm2710-rpi-3-b-plus.dtb b/targets/bcm2710-rpi-3-b-plus.dtb index bdc7e43f05cfe1f2d4001627672783f06ed9a155..c842951c7f8ed3f9061adf59c192059addb0a183 100644 GIT binary patch literal 28599 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 literal 27082 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 B18)ET From 7e69227fdcab959a6a9f88b0ec8b1b97733bfa43 Mon Sep 17 00:00:00 2001 From: Berkus Decker Date: Sun, 12 Jun 2022 23:02:47 +0300 Subject: [PATCH 027/107] wip: chainloader must pass through the DTB --- bin/chainboot/src/main.rs | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/bin/chainboot/src/main.rs b/bin/chainboot/src/main.rs index 5191391cd..9697b1f1c 100644 --- a/bin/chainboot/src/main.rs +++ b/bin/chainboot/src/main.rs @@ -20,7 +20,7 @@ use { /// - Only a single core must be active and running this function. /// - The init calls in this function must appear in the correct order. #[unsafe(no_mangle)] -unsafe extern "C" fn kernel_init(_dtb: u32, max_kernel_size: u64) -> ! { +unsafe extern "C" fn kernel_init(dtb: u32, max_kernel_size: u64) -> ! { #[cfg(feature = "jtag")] libmachine::debug::jtag::wait_debugger(); @@ -38,7 +38,7 @@ unsafe extern "C" fn kernel_init(_dtb: u32, max_kernel_size: u64) -> ! { // println! is usable from here on. // Transition from unsafe to safe. - kernel_main(max_kernel_size) + kernel_main(dtb, max_kernel_size) } // https://onlineasciitools.com/convert-text-to-ascii-art (FIGlet) with `cricket` font @@ -63,9 +63,13 @@ fn read_u64() -> u64 { /// The main function running after the early init. #[inline(always)] -fn kernel_main(max_kernel_size: u64) -> ! { +fn kernel_main(dtb: u32, max_kernel_size: u64) -> ! { + #[cfg(test)] + test_main(); + print!("{}", LOGO); println!("{:>51}\n", BcmHost::board_name()); + println!("Preserving DTB at {:8x}", dtb); println!("⏪ Requesting kernel image..."); let kernel_addr: *mut u8 = BcmHost::kernel_load_address() as *mut u8; @@ -136,13 +140,13 @@ fn kernel_main(max_kernel_size: u64) -> ! { // Use black magic to create a function pointer. // SAFETY: We're getting to safety soon! - let kernel: fn() -> ! = unsafe { core::mem::transmute(kernel_addr) }; + let kernel: fn(u32) -> ! = unsafe { core::mem::transmute(kernel_addr) }; // Force everything to complete before we jump. barrier::isb(barrier::SY); // Jump to loaded kernel! - kernel() + kernel(dtb) } #[panic_handler] From ce783efbcad71585b48499d0fd249e506bb52f8f Mon Sep 17 00:00:00 2001 From: Berkus Decker Date: Tue, 21 Jun 2022 21:25:22 +0300 Subject: [PATCH 028/107] wip: add payload iter tests - @todo finish them --- kernel/init_thread/src/device_tree.rs | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/kernel/init_thread/src/device_tree.rs b/kernel/init_thread/src/device_tree.rs index 9ceae1594..6fc6ac37d 100644 --- a/kernel/init_thread/src/device_tree.rs +++ b/kernel/init_thread/src/device_tree.rs @@ -231,6 +231,33 @@ impl<'a, 'i: 'a, 'dt: 'i> Iterator for PayloadPairsIter<'a, 'i, 'dt> { } } +#[cfg(test)] +mod tests { + use super::PayloadPairsIter; + + const BUF: [u32; 4] = [0x0000_0000, 0x2000_0000, 0x4000_0000, 0x8000_0000]; + + #[test_case] + fn parse_1_1_prop_correctly() { + PayloadPairsIter + } + + #[test_case] + fn parse_1_2_prop_correctly() { + PayloadPairsIter + } + + #[test_case] + fn parse_2_1_prop_correctly() { + PayloadPairsIter + } + + #[test_case] + fn parse_2_2_prop_correctly() { + PayloadPairsIter + } +} + // See "2.2.3 Path Names" in DTSpec v0.3 // This is based on https://lib.rs/dtb implementation (c) Simon Prykhodko, MIT license. struct PathSplit<'a> { From 7eaf888efbd0b485536b1acbf1766c2ddabde5e9 Mon Sep 17 00:00:00 2001 From: Berkus Decker Date: Tue, 21 Jun 2022 21:25:49 +0300 Subject: [PATCH 029/107] wip: add dump of entire live FDT from the board --- kernel/init_thread/src/device_tree.rs | 150 +++++++++++++++++++++++++- kernel/init_thread/src/main.rs | 49 +++++---- 2 files changed, 173 insertions(+), 26 deletions(-) diff --git a/kernel/init_thread/src/device_tree.rs b/kernel/init_thread/src/device_tree.rs index 6fc6ac37d..92caf7ede 100644 --- a/kernel/init_thread/src/device_tree.rs +++ b/kernel/init_thread/src/device_tree.rs @@ -1,12 +1,12 @@ #![allow(dead_code)] use { - core::alloc::Layout, + core::{alloc::Layout, ptr::read_unaligned}, fdt_rs::{ - base::DevTree, - error::DevTreeError, + base::{DevTree, iters::StringPropIter}, + error::{DevTreeError, Result as DevTreeResult}, index::{DevTreeIndex, DevTreeIndexNode, DevTreeIndexProp}, - prelude::PropReader, + prelude::{FallibleIterator, PropReader}, }, shrinkwraprs::Shrinkwrap, }; @@ -56,6 +56,13 @@ pub fn get_size_cells<'a, 'i: 'a, 'dt: 'i>(node: DevTreeIndexNode<'a, 'i, 'dt>) pub struct DeviceTree<'a>(DevTreeIndex<'a, 'a>); impl<'a> DeviceTree<'a> { + pub fn dumper(&'a self, indent: usize) -> FdtDumper<'a> { + FdtDumper { + index: &self.0, + indent, + } + } + pub fn layout(tree: DevTree<'a>) -> Result { DevTreeIndex::get_layout(&tree) } @@ -66,7 +73,10 @@ impl<'a> DeviceTree<'a> { // @todo drop all the wrapper shenanigans and just export this one fn /// Iterate path separated by / starting from the root "/" and find props one by one. - pub fn get_prop_by_path(&self, path: &str) -> Result { + pub fn get_prop_by_path( + &self, + path: &str, + ) -> Result, DevTreeError> { let mut path = PathSplit::new(path); let mut node_iter = self.0.root().children(); let mut node: Option = Some(self.0.root()); @@ -180,6 +190,7 @@ impl<'a, 'i: 'a, 'dt: 'i> Iterator for PayloadPairsIter<'a, 'i, 'dt> { type Item = (u64, u64); fn next(&mut self) -> Option { + libqemu::semi_println!("Offset {}, total {}", self.offset, self.total); if self.offset >= self.total { // @todo check for sufficient space for the following read or the reads below may fail! return None; @@ -372,3 +383,132 @@ mod tests { assert_eq!(path.move_next(), false); } } + +//================================================================================================= +// Dump the entire FDT +// From https://github.com/rs-embedded/fdtdump/blob/master/src/main.rs +//================================================================================================= + +fn are_printable_strings(mut prop_iter: StringPropIter) -> bool { + loop { + match prop_iter.next() { + Ok(Some(s_ref)) => { + if s_ref.is_empty() { + return false; + } + } + Ok(None) => return true, + Err(_) => return false, + } + } +} + +pub struct FdtDumper<'a> { + index: &'a DevTreeIndex<'a, 'a>, + indent: usize, +} + +impl<'i, 'dt> FdtDumper<'_> { + fn push_indent(&mut self) { + for _ in 0..self.indent { + libqemu::semi_print!(" "); + } + } + + fn dump_node_name(&mut self, name: &str) { + self.push_indent(); + libqemu::semi_println!("{name} {{"); + } + + fn dump_node(&mut self, node: &DevTreeIndexNode) -> DevTreeResult<()> { + let mut name = node.name()?; + if name.is_empty() { + name = "/"; + } else { + name = node.name()?; + } + self.dump_node_name(name); + Ok(()) + } + + fn dump_property(&mut self, prop: DevTreeIndexProp) -> DevTreeResult<()> { + self.push_indent(); + + libqemu::semi_print!("{}", prop.name()?); + + if prop.length() == 0 { + libqemu::semi_println!(";"); + return Ok(()); + } + libqemu::semi_print!(" = "); + + // Unsafe Ok - we're reinterpreting the data as expected. + unsafe { + // First try to parse as an array of strings + if are_printable_strings(prop.iter_str()) { + let mut iter = prop.iter_str(); + while let Some(s) = iter.next()? { + libqemu::semi_print!("\"{}\", ", s); + } + // let _ = self.dump.pop(); + // let _ = self.dump.pop(); + } else if prop.propbuf().len() % size_of::() == 0 { + libqemu::semi_print!("<"); + for val in prop.propbuf().chunks_exact(size_of::()) { + // We use read_unaligned + #[allow(clippy::cast_ptr_alignment)] + let v = read_unaligned::(val.as_ptr() as *const u32); + let v = u32::from_be(v); + libqemu::semi_print!("{:#010x} ", v); + } + // let _ = self.dump.pop(); // Pop off extra space + libqemu::semi_print!(">"); + } else { + libqemu::semi_print!("["); + for val in prop.propbuf() { + libqemu::semi_print!("{:02x} ", val); + } + // let _ = self.dump.pop(); // Pop off extra space + libqemu::semi_print!("]"); + } + } + + libqemu::semi_println!(";"); + Ok(()) + } + + pub fn dump_level(&mut self, node: &DevTreeIndexNode) -> DevTreeResult<()> { + self.dump_node(node)?; + self.indent += 1; + for prop in node.props() { + let _ = self.dump_property(prop)?; + } + for child in node.children() { + let _ = self.dump_level(&child)?; + } + self.indent -= 1; + self.push_indent(); + libqemu::semi_println!("}};"); + Ok(()) + } + + pub fn dump_root(&mut self) -> DevTreeResult<()> { + self.dump_level(&self.index.root()) + } + + pub fn dump_metadata(&mut self) { + let fdt = self.index.fdt(); + libqemu::semi_println!("// magic:\t\t{:#x}", fdt.magic()); + let s = fdt.totalsize(); + libqemu::semi_println!("// totalsize:\t\t{:#x} ({})", s, s); + libqemu::semi_println!("// off_dt_struct:\t{:#x}", fdt.off_dt_struct()); + libqemu::semi_println!("// off_dt_strings:\t{:#x}", fdt.off_dt_strings()); + libqemu::semi_println!("// off_mem_rsvmap:\t{:#x}", fdt.off_mem_rsvmap()); + libqemu::semi_println!("// version:\t\t{:}", fdt.version()); + libqemu::semi_println!("// last_comp_version:\t{:}", fdt.last_comp_version()); + libqemu::semi_println!("// boot_cpuid_phys:\t{:#x}", fdt.boot_cpuid_phys()); + libqemu::semi_println!("// size_dt_strings:\t{:#x}", fdt.size_dt_strings()); + libqemu::semi_println!("// size_dt_struct:\t{:#x}", fdt.size_dt_struct()); + libqemu::semi_println!(""); + } +} diff --git a/kernel/init_thread/src/main.rs b/kernel/init_thread/src/main.rs index 6c9ff9fa2..07931e962 100644 --- a/kernel/init_thread/src/main.rs +++ b/kernel/init_thread/src/main.rs @@ -44,10 +44,11 @@ mod paging; mod syscall_test; use { - core::{alloc::Allocator, panic::PanicInfo, ptr::write_bytes}, + core::{panic::PanicInfo, ptr::write_bytes, slice}, device_tree::{DeviceTree, DeviceTreeProp}, fdt_rs::{ base::DevTree, + error::DevTreeError, prelude::{FallibleIterator, PropReader}, }, libcpu::endless_sleep, @@ -71,7 +72,7 @@ fn panic(info: &PanicInfo) -> ! { fn dump_memory_map() { // Output the memory map as we could derive from FDT and information about our loaded image // Use it to imagine how the memmap would look like in the end. - arch::memory::print_layout(); + // arch::memory::print_layout(); } #[unsafe(no_mangle)] @@ -91,6 +92,22 @@ pub extern "C" fn init_main(dtb_ptr: *const u8) -> ! { // early_uart_init(0xFE20_1000); semi_println!("DTB at physical: {:#016X}", dtb_ptr as u64); + // ───────────────────────────────────────────────────────────────────── + // Start bump allocator + // ───────────────────────────────────────────────────────────────────── + + let init_start = unsafe { &__init_start as *const u8 as u64 }; + let init_end = unsafe { &__init_end as *const u8 as u64 }; + let free_start = unsafe { &__free_memory_start as *const u8 as u64 }; + + let memory_size = 256 * 1024 * 1024; + let mut allocator = BootAllocator::new(PhysAddr::new(free_start), memory_size); + let memory_end = allocator.end(); + semi_println!( + "init_main: Created BootAllocator {memory_size} @ {:#016X}", + free_start + ); + // ───────────────────────────────────────────────────────────────────── // Parse Device Tree // ───────────────────────────────────────────────────────────────────── @@ -105,17 +122,23 @@ pub extern "C" fn init_main(dtb_ptr: *const u8) -> ! { let layout = DeviceTree::layout(device_tree).expect("Couldn't calculate DeviceTree index"); let block = allocator - .alloc(layout.size) + .alloc_aligned(layout.size(), layout.align()) .expect("Couldn't allocate DeviceTree index"); + let raw_slice = unsafe { core::slice::from_raw_parts_mut(block.0 as *mut u8, layout.size()) }; let device_tree = - DeviceTree::new(device_tree, block).expect("Couldn't initialize indexed DeviceTree"); + DeviceTree::new(device_tree, raw_slice).expect("Couldn't initialize indexed DeviceTree"); let board = device_tree.get_prop_by_path("/model").unwrap().str(); if let Ok(board_name) = board { semi_println!("Running on {board_name}"); } + let mut dumper = device_tree.dumper(0); + + dumper.dump_metadata(); + dumper.dump_root().expect("oof"); + // To init memory allocation we need to parse memory regions from dtb and add the regions to // available memory regions list. Then initial BootRegionAllocator will get memory from these // regions and record their usage into some OTHER structures, removing these allocations from @@ -170,7 +193,7 @@ pub extern "C" fn init_main(dtb_ptr: *const u8) -> ! { semi_println!( "DTB region: {} bytes at {:x}", device_tree.fdt().totalsize(), - dtb + dtb_ptr as usize ); dump_memory_map(); @@ -215,22 +238,6 @@ pub extern "C" fn init_main(dtb_ptr: *const u8) -> ! { // } // } - // ───────────────────────────────────────────────────────────────────── - // Further init - // ───────────────────────────────────────────────────────────────────── - - let init_start = unsafe { &__init_start as *const u8 as u64 }; - let init_end = unsafe { &__init_end as *const u8 as u64 }; - let free_start = unsafe { &__free_memory_start as *const u8 as u64 }; - - let memory_size = 256 * 1024 * 1024; - let mut allocator = BootAllocator::new(PhysAddr::new(free_start), memory_size); - let memory_end = allocator.end(); - semi_println!( - "init_main: Created BootAllocator {memory_size} @ {:#016X}", - free_start - ); - // ═══════════════════════════════════════════════════════════════ // PHASE 1: Load kernel // ═══════════════════════════════════════════════════════════════ From 3d170eb7a99e73537fe395498681aa8a52d81dd6 Mon Sep 17 00:00:00 2001 From: Berkus Decker Date: Mon, 26 Jan 2026 01:34:31 +0200 Subject: [PATCH 030/107] =?UTF-8?q?wip:=20parse=20DTB=20=E2=AC=87=EF=B8=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit From 472599e597a0fe8f48b2a34282e3faaa71f27acc Mon Sep 17 00:00:00 2001 From: Berkus Decker Date: Mon, 26 Jan 2026 20:29:32 +0200 Subject: [PATCH 031/107] wip: convert QEMU write0 call argument to CStr for stronger type guarantees move and enlarge stack to avoid crash (?) empty semi_println!() fails if we just pass c"\n" to write0 call, use full write buffer instead --- Cargo.toml | 2 +- kernel/init_thread/init_thread.ld | 16 +++--------- kernel/init_thread/src/device_tree.rs | 2 +- kernel/init_thread/src/main.rs | 36 ++++++++++++++------------- kernel/init_thread/src/memory.rs | 8 ++++++ libs/print/src/lib.rs | 13 +++++----- libs/qemu/src/lib.rs | 17 +++++++++---- 7 files changed, 52 insertions(+), 42 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 5da79eacd..417832361 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -110,7 +110,7 @@ codegen-units = 1 debug = true lto = true opt-level = 's' -panic = "abort" +# panic = "abort" # - debug in qemu - wanna see panics [profile.release-fast] codegen-units = 16 diff --git a/kernel/init_thread/init_thread.ld b/kernel/init_thread/init_thread.ld index e853ec239..3abdf39fd 100644 --- a/kernel/init_thread/init_thread.ld +++ b/kernel/init_thread/init_thread.ld @@ -7,7 +7,11 @@ INIT_BASE = 0x80000; SECTIONS { + /* Stack for init thread (grows down) */ + . = 0; + __stack_bottom = .; . = INIT_BASE; + __stack_top = .; __init_start = .; @@ -45,18 +49,6 @@ SECTIONS . = ALIGN(4K); __init_end = .; - /* Stack for init thread (grows down) */ - . = ALIGN(16); - __stack_bottom = .; - . += 64K; - __stack_top = .; - - /* Page tables - we allocate space for them here */ - . = ALIGN(4K); - __page_tables_start = .; - . += 256K; /* Space for page tables */ - __page_tables_end = .; - /* Free memory starts here - used for kernel placement */ . = ALIGN(2M); /* 2MB aligned for huge pages if desired */ __free_memory_start = .; diff --git a/kernel/init_thread/src/device_tree.rs b/kernel/init_thread/src/device_tree.rs index 92caf7ede..a3707b6d0 100644 --- a/kernel/init_thread/src/device_tree.rs +++ b/kernel/init_thread/src/device_tree.rs @@ -509,6 +509,6 @@ impl<'i, 'dt> FdtDumper<'_> { libqemu::semi_println!("// boot_cpuid_phys:\t{:#x}", fdt.boot_cpuid_phys()); libqemu::semi_println!("// size_dt_strings:\t{:#x}", fdt.size_dt_strings()); libqemu::semi_println!("// size_dt_struct:\t{:#x}", fdt.size_dt_struct()); - libqemu::semi_println!(""); + libqemu::semi_println!(); } } diff --git a/kernel/init_thread/src/main.rs b/kernel/init_thread/src/main.rs index 07931e962..114db856a 100644 --- a/kernel/init_thread/src/main.rs +++ b/kernel/init_thread/src/main.rs @@ -66,14 +66,14 @@ unsafe extern "C" { #[panic_handler] fn panic(info: &PanicInfo) -> ! { semi_println!("PANICKED: {info}"); - endless_sleep() + libqemu::semihosting::exit_failure() } -fn dump_memory_map() { - // Output the memory map as we could derive from FDT and information about our loaded image - // Use it to imagine how the memmap would look like in the end. - // arch::memory::print_layout(); -} +// fn dump_memory_map() { +// Output the memory map as we could derive from FDT and information about our loaded image +// Use it to imagine how the memmap would look like in the end. +// arch::memory::print_layout(); +// } #[unsafe(no_mangle)] pub extern "C" fn init_main(dtb_ptr: *const u8) -> ! { @@ -90,7 +90,7 @@ pub extern "C" fn init_main(dtb_ptr: *const u8) -> ! { // Hardcoded UART address for early boot (RPi4: 0xFE201000) // Will be properly mapped later // early_uart_init(0xFE20_1000); - semi_println!("DTB at physical: {:#016X}", dtb_ptr as u64); + semi_println!("DTB at physical: {:#016x}", dtb_ptr as u64); // ───────────────────────────────────────────────────────────────────── // Start bump allocator @@ -104,7 +104,7 @@ pub extern "C" fn init_main(dtb_ptr: *const u8) -> ! { let mut allocator = BootAllocator::new(PhysAddr::new(free_start), memory_size); let memory_end = allocator.end(); semi_println!( - "init_main: Created BootAllocator {memory_size} @ {:#016X}", + "init_main: Created BootAllocator {memory_size} @ {:#016x}", free_start ); @@ -137,7 +137,7 @@ pub extern "C" fn init_main(dtb_ptr: *const u8) -> ! { let mut dumper = device_tree.dumper(0); dumper.dump_metadata(); - dumper.dump_root().expect("oof"); + // dumper.dump_root().expect("oof"); // To init memory allocation we need to parse memory regions from dtb and add the regions to // available memory regions list. Then initial BootRegionAllocator will get memory from these @@ -185,18 +185,18 @@ pub extern "C" fn init_main(dtb_ptr: *const u8) -> ! { // memreserve = <0x3b400000 0x04c00000 >; // Iterate compatible nodes (example): - // for entry in device_tree.compatible_nodes("arm,pl011") { - // semi_println!("reserved: {:?} (bytes at ?)", entry.name()/*, entry.address*/); - // } + for entry in device_tree.compatible_nodes("arm,pl011") { + semi_println!("PL011 device: {:?}", entry.name() /*, entry.address*/); + } // 6. Also, remove the DTB memory region + index semi_println!( - "DTB region: {} bytes at {:x}", + "DTB region: {} bytes at {:#016x}", device_tree.fdt().totalsize(), dtb_ptr as usize - ); + ); // also include the raw_slice allocated bit - dump_memory_map(); + // dump_memory_map(); // Next step: parse DTB! // unsafe { @@ -242,6 +242,8 @@ pub extern "C" fn init_main(dtb_ptr: *const u8) -> ! { // PHASE 1: Load kernel // ═══════════════════════════════════════════════════════════════ + semi_println!("init_main: Load kernel"); + let kernel_layout = loader::load_kernel(&mut allocator).expect("Failed to load nucleus"); semi_println!("init_main: Loaded nucleus image"); @@ -268,7 +270,7 @@ pub extern "C" fn init_main(dtb_ptr: *const u8) -> ! { let ttbr0 = mmu_setup.ttbr0(); let ttbr1 = mmu_setup.ttbr1(); - semi_println!("init_main: TTBR0_EL1 at {ttbr0:#016X}, TTBR1_EL1 at {ttbr1:#016X}"); + semi_println!("init_main: TTBR0_EL1 at {ttbr0:#016x}, TTBR1_EL1 at {ttbr1:#016x}"); // Get vector table virtual address for VBAR_EL1 // VBAR is only used after MMU is enabled, so we set the virtual address directly @@ -280,7 +282,7 @@ pub extern "C" fn init_main(dtb_ptr: *const u8) -> ! { .expect("Failed to allocate EL1 stack"); let el1_stack_top = el1_stack.as_u64() + 16 * 4096; // FIXME: stack must be identity-mapped! - semi_println!("init_main: EL1 stack at {el1_stack_top:#016X}, vbar {vbar:#016X}"); + semi_println!("init_main: EL1 stack at {el1_stack_top:#016x}, vbar {vbar:#016x}"); // ═══════════════════════════════════════════════════════════════ // PHASE 4: Enable MMU and drop to EL1 diff --git a/kernel/init_thread/src/memory.rs b/kernel/init_thread/src/memory.rs index 3f726aaab..a421ec26b 100644 --- a/kernel/init_thread/src/memory.rs +++ b/kernel/init_thread/src/memory.rs @@ -78,6 +78,14 @@ impl BootAllocator { pub fn alloc_aligned(&mut self, size: usize, align: usize) -> Option { let aligned = self.current.align_up(align as u64); let new_current = PhysAddr(aligned.0 + size as u64); + + libqemu::semi_println!( + "alloc_aligned {:#016x} => {:#016x} (wrt {:#016x})", + aligned.0, + new_current.0, + self.end.0 + ); + if new_current > self.end { return None; } diff --git a/libs/print/src/lib.rs b/libs/print/src/lib.rs index 77ec5e7d7..0050c1b7a 100644 --- a/libs/print/src/lib.rs +++ b/libs/print/src/lib.rs @@ -5,11 +5,12 @@ #![no_std] -/// No-alloc write!() implementation from -/// Requires you to allocate a buffer somewhere manually (usually, on stack). +// No-alloc write!() implementation from +// Requires you to allocate a buffer somewhere manually (usually, on stack). // @todo Try to use arrayvec::ArrayString here instead? // @todo probably use defmt for comms with host? -use core::{cmp::min, fmt}; + +use core::{cmp::min, ffi::CStr, fmt}; struct WriteTo<'a> { buffer: &'a mut [u8], @@ -32,12 +33,12 @@ impl<'a> WriteTo<'a> { } #[allow(unused)] - pub fn into_cstr(self) -> Option<&'a str> { + pub fn into_cstr(self) -> Option<&'a CStr> { (self.used < self.buffer.len()).then(|| { // Terminate the string self.buffer[self.used] = 0; // SAFETY: only successful concats of str - must be a valid str. - unsafe { core::str::from_utf8_unchecked(&self.buffer[..=self.used]) } + unsafe { CStr::from_bytes_with_nul_unchecked(&self.buffer[..=self.used]) } }) } } @@ -67,7 +68,7 @@ pub fn format_str<'a>(buffer: &'a mut [u8], args: fmt::Arguments) -> Result<&'a } // Return a zero-terminated str -pub fn format_cstr<'a>(buffer: &'a mut [u8], args: fmt::Arguments) -> Result<&'a str, fmt::Error> { +pub fn format_cstr<'a>(buffer: &'a mut [u8], args: fmt::Arguments) -> Result<&'a CStr, fmt::Error> { let mut w = WriteTo::new(buffer); fmt::write(&mut w, args)?; w.into_cstr().ok_or(fmt::Error) diff --git a/libs/qemu/src/lib.rs b/libs/qemu/src/lib.rs index 9873cb3d3..e4c4227f7 100644 --- a/libs/qemu/src/lib.rs +++ b/libs/qemu/src/lib.rs @@ -29,9 +29,9 @@ pub mod semihosting { qemu_exit_handle.exit_failure() } - pub fn sys_write0_call(text: &str) { + pub fn sys_write0_call(text: &core::ffi::CStr) { let cmd = 0x04; - // SAFETY: text must be \0-terminated! + // SAFETY: text must be \0-terminated, which CStr above shall ensure. unsafe { core::arch::asm!( "hlt #0xF000" @@ -49,17 +49,24 @@ pub mod semihosting { libqemu::semihosting::sys_write0_call( libprint::format_cstr(&mut buf, core::format_args!($($arg)+)).unwrap(), ); - } + }; } #[macro_export] macro_rules! semi_println { - // early_println!("a {} event", "log") + // semi_println!() + () => { + let mut buf = [0_u8; 4096]; // Increase this buffer size to allow dumping larger panic texts. + libqemu::semihosting::sys_write0_call( + libprint::format_cstr(&mut buf, core::format_args_nl!("")).unwrap(), + ); + }; + // semi_println!("a {} event", "log") ($($arg:tt)+) => { let mut buf = [0_u8; 4096]; // Increase this buffer size to allow dumping larger panic texts. libqemu::semihosting::sys_write0_call( libprint::format_cstr(&mut buf, core::format_args_nl!($($arg)+)).unwrap(), ); - } + }; } } From 477398e60cf564d3c691db2f1b357e63b98d92d4 Mon Sep 17 00:00:00 2001 From: Berkus Decker Date: Mon, 26 Jan 2026 22:21:30 +0200 Subject: [PATCH 032/107] wip: Dump FDT --- kernel/init_thread/src/main.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kernel/init_thread/src/main.rs b/kernel/init_thread/src/main.rs index 114db856a..3a51ac28d 100644 --- a/kernel/init_thread/src/main.rs +++ b/kernel/init_thread/src/main.rs @@ -137,7 +137,7 @@ pub extern "C" fn init_main(dtb_ptr: *const u8) -> ! { let mut dumper = device_tree.dumper(0); dumper.dump_metadata(); - // dumper.dump_root().expect("oof"); + dumper.dump_root().expect("oof"); // To init memory allocation we need to parse memory regions from dtb and add the regions to // available memory regions list. Then initial BootRegionAllocator will get memory from these From ef85a7d499ec558f1c4d8b6443a3e61f0a087fbe Mon Sep 17 00:00:00 2001 From: Berkus Decker Date: Mon, 26 Jan 2026 22:32:51 +0200 Subject: [PATCH 033/107] wip: exit qemu automatically --- kernel/init_thread/src/main.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kernel/init_thread/src/main.rs b/kernel/init_thread/src/main.rs index 3a51ac28d..5abde76eb 100644 --- a/kernel/init_thread/src/main.rs +++ b/kernel/init_thread/src/main.rs @@ -437,7 +437,7 @@ pub extern "C" fn init_thread_run(_dtb_ptr: *const u8) -> ! { // // Kernel high map (TTBR1) is ready for when init makes syscalls // // This never returns // switch_to_domain(init_domain, init_time); - endless_sleep() + libqemu::semihosting::exit_success() } /* // ───────────────────────────────────────────────────────────────────── From 63d112ea7957799cd59e9c649681596c3edb1930 Mon Sep 17 00:00:00 2001 From: Berkus Decker Date: Mon, 26 Jan 2026 22:49:17 +0200 Subject: [PATCH 034/107] wip: update plan progress --- kernel/Plan.md | 34 +++++++++++++++------------------- 1 file changed, 15 insertions(+), 19 deletions(-) diff --git a/kernel/Plan.md b/kernel/Plan.md index ae7c12be8..4af11884d 100644 --- a/kernel/Plan.md +++ b/kernel/Plan.md @@ -5,30 +5,26 @@ What's needed: quick-n-dirty higher-half mappings setup and kernel physical memo - `__init_thread_start` till `__init_thread_end` identity-map (no code/data split yet?) Steps: -- Buildable -- Print that we can invoke kernel function using a syscall from the init_thread (even it if runs at the same EL for now) - -- Run steps by step - - Enter kernel init in EL2 - this will be needed to set up kernel mappings - - Print DTB - - Print max RAM from DTB - - Print kernel covered area - - Print KERNEL_HIGH_BASE - - Print kernel mappings size and attribs - - Print init_thread covered area - - Print init_thread mappings size - +- [x] Buildable - [x] build all shit together into a binary - [x] make separate init_thread section and start booting from init_thread - [x] make early_print work -- [ ] parse dtb - +- [x] parse dtb +- [x] Print that we can invoke kernel function using a syscall from the init_thread (even it if runs at the same EL for now) +- [x] Enter kernel init in EL2 - this will be needed to set up kernel mappings +- [x] Print DTB +- [x] Print max RAM from DTB +- [ ] Print kernel covered area +- [ ] Print KERNEL_HIGH_BASE +- [ ] Print kernel mappings size and attribs +- [ ] Print init_thread covered area +- [ ] Print init_thread mappings size +- [ ] Make some caps work - Untypeds, Domains, Buffers, what else? - START FILLING IN CAPS - - untypeds - - init_thread context and domain - + - [ ] untypeds + - [ ] init_thread context and domain Whatever kernel links must also be located in high-mem mapping, so we cannot share this code with init_thread at all! This means it's probably sensible to build kernel as a separate ELF file linked entirely high, then merge it with the init_thread binary through specially-named sections; there should be no symbol resolution across two binaries, so the nucleus image is solely pulled via it's PHDRS (but we need to place the BSS which will be erased by the init_thread before turning the MMU on) -See gh:metta-systems/kernel-embed-prototype for an outline of this approach - copy it here and lets go. +- [x] See gh:metta-systems/kernel-embed-prototype for an outline of this approach - copy it here and lets go. From b2f7bd9c1f1946231e518f71d54c17c06d5dc860 Mon Sep 17 00:00:00 2001 From: Berkus Decker Date: Sun, 22 Feb 2026 23:06:42 +0200 Subject: [PATCH 035/107] =?UTF-8?q?wip:=20boot=5Finfo=20=E2=AC=86=EF=B8=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit From 3f0f7da831527b2db2d3f5dc8b8afc42862539fe Mon Sep 17 00:00:00 2001 From: Berkus Decker Date: Mon, 26 Jan 2026 22:57:48 +0200 Subject: [PATCH 036/107] wip: move new version of boot_info aside, prior to merging the old one --- kernel/init_thread/src/boot_info.rs | 191 ------------------ kernel/init_thread/src/boot_info_.rs | 45 +++++ .../memory/test}/memory_test.rs | 0 3 files changed, 45 insertions(+), 191 deletions(-) delete mode 100644 kernel/init_thread/src/boot_info.rs create mode 100644 kernel/init_thread/src/boot_info_.rs rename {libmemory => libs/memory/test}/memory_test.rs (100%) diff --git a/kernel/init_thread/src/boot_info.rs b/kernel/init_thread/src/boot_info.rs deleted file mode 100644 index 03e5b9c8d..000000000 --- a/kernel/init_thread/src/boot_info.rs +++ /dev/null @@ -1,191 +0,0 @@ -#![allow(dead_code)] - -use crate::{memory::PhysAddr, println, sync}; - -#[derive(Default, Copy, Clone)] -struct BootInfoMemRegion { - pub start: PhysAddr, - pub end: PhysAddr, -} - -impl BootInfoMemRegion { - pub const fn new() -> BootInfoMemRegion { - BootInfoMemRegion { - start: PhysAddr::zero(), - end: PhysAddr::zero(), - } - } - - pub fn size(&self) -> u64 { - self.end - self.start - } - - pub fn is_empty(&self) -> bool { - self.start == self.end - } -} - -const NUM_MEM_REGIONS: usize = 16; - -pub enum BootInfoError { - NoFreeMemRegions, -} - -#[derive(Default)] -struct BootInfo { - pub regions: [BootInfoMemRegion; NUM_MEM_REGIONS], - pub max_slot_pos: usize, -} - -impl BootInfo { - pub const fn new() -> BootInfo { - BootInfo { - regions: [BootInfoMemRegion::new(); NUM_MEM_REGIONS], - max_slot_pos: 0, - } - } - - pub fn insert_region(&mut self, reg: BootInfoMemRegion) -> Result<(), BootInfoError> { - if reg.is_empty() { - return Ok(()); - } - assert!(reg.start <= reg.end); - for region in self.regions.iter_mut() { - if region.is_empty() { - *region = reg; - return Ok(()); - } - } - return Err(BootInfoError::NoFreeMemRegions); - } - - pub fn alloc_region(&mut self, size_bits: usize) -> Result { - let mut reg_index: usize = 0; - let mut reg: BootInfoMemRegion = BootInfoMemRegion::new(); - let mut rem_small: BootInfoMemRegion = BootInfoMemRegion::new(); - let mut rem_large: BootInfoMemRegion = BootInfoMemRegion::new(); - /* - * Search for a free mem region that will be the best fit for an allocation. We favour allocations - * that are aligned to either end of the region. If an allocation must split a region we favour - * an unbalanced split. In both cases we attempt to use the smallest region possible. In general - * this means we aim to make the size of the smallest remaining region smaller (ideally zero) - * followed by making the size of the largest remaining region smaller. - */ - for (i, reg_iter) in self.regions.iter().enumerate() { - let mut new_reg: BootInfoMemRegion = BootInfoMemRegion::new(); - - /* Determine whether placing the region at the start or the end will create a bigger left over region */ - if reg_iter.start.aligned_up(1usize << size_bits) - reg_iter.start - < reg_iter.end - reg_iter.end.aligned_down(1usize << size_bits) - { - new_reg.start = reg_iter.start.aligned_up(1usize << size_bits); - new_reg.end = new_reg.start + (1u64 << size_bits); - } else { - new_reg.end = reg_iter.end.aligned_down(1usize << size_bits); - new_reg.start = new_reg.end - (1u64 << size_bits); - } - if new_reg.end > new_reg.start - && new_reg.start >= reg_iter.start - && new_reg.end <= reg_iter.end - { - let mut new_rem_small: BootInfoMemRegion = BootInfoMemRegion::new(); - let mut new_rem_large: BootInfoMemRegion = BootInfoMemRegion::new(); - - if new_reg.start - reg_iter.start < reg_iter.end - new_reg.end { - new_rem_small.start = reg_iter.start; - new_rem_small.end = new_reg.start; - new_rem_large.start = new_reg.end; - new_rem_large.end = reg_iter.end; - } else { - new_rem_large.start = reg_iter.start; - new_rem_large.end = new_reg.start; - new_rem_small.start = new_reg.end; - new_rem_small.end = reg_iter.end; - } - if reg.is_empty() - || (new_rem_small.size() < rem_small.size()) - || (new_rem_small.size() == rem_small.size() - && new_rem_large.size() < rem_large.size()) - { - reg = new_reg; - rem_small = new_rem_small; - rem_large = new_rem_large; - reg_index = i; - } - } - } - if reg.is_empty() { - panic!("Kernel init failed: not enough memory\n"); - } - /* Remove the region in question */ - self.regions[reg_index] = BootInfoMemRegion::new(); - /* Add the remaining regions in largest to smallest order */ - self.insert_region(rem_large)?; - if self.insert_region(rem_small).is_err() { - println!( - "BootInfo::alloc_region(): wasted {} bytes due to alignment, try to increase NUM_MEM_REGIONS", - rem_small.size() - ); - } - Ok(reg.start) - } -} - -#[link_section = ".data.boot"] // @todo put zero-initialized stuff to .bss.boot! -static BOOT_INFO: sync::NullLock = sync::NullLock::new(BootInfo::new()); - -// --- -// --- -// --- -// --- -// --- -// --- -// --- -// --- -// --- - -/// Boot info collected during early init -/// Lives in .data section (not heap - no allocator yet!) -#[repr(C)] -struct BootInfo { - dtb_phys: PhysAddr, - dtb_size: usize, - - // Memory regions from DTB - memory_regions: [MemoryRegion; 16], - memory_region_count: usize, - - // Reserved regions (kernel image, DTB, modules) - reserved_regions: [ReservedRegion; 32], - reserved_region_count: usize, - - // Loaded modules (init process, drivers) - modules: [LoadedModule; 8], - module_count: usize, - // Kernel image bounds - // kernel_phys_start: PhysAddr, - // kernel_phys_end: PhysAddr, - - // Init thread stack (will be reclaimed) - // init_stack_phys: PhysAddr, - // init_stack_size: usize, -} - -#[repr(C)] -struct MemoryRegion { - base: PhysAddr, - size: usize, - flags: MemoryFlags, -} - -#[repr(C)] -struct LoadedModule { - name: [u8; 32], - phys_start: PhysAddr, - size: usize, - entry_point: u64, // Offset from phys_start -} - -/// Static boot info - filled during early init -#[unsafe(link_section = ".init_thread.bss")] -static mut BOOT_INFO: BootInfo = BootInfo::zeroed(); diff --git a/kernel/init_thread/src/boot_info_.rs b/kernel/init_thread/src/boot_info_.rs new file mode 100644 index 000000000..4fb7bf73c --- /dev/null +++ b/kernel/init_thread/src/boot_info_.rs @@ -0,0 +1,45 @@ +/// Boot info collected during early init +/// Lives in .data section (not heap - no allocator yet!) +#[repr(C)] +struct BootInfo { + dtb_phys: PhysAddr, + dtb_size: usize, + + // Memory regions from DTB + memory_regions: [MemoryRegion; 16], + memory_region_count: usize, + + // Reserved regions (kernel image, DTB, modules) + reserved_regions: [ReservedRegion; 32], + reserved_region_count: usize, + + // Loaded modules (init process, drivers) + modules: [LoadedModule; 8], + module_count: usize, + // Kernel image bounds + // kernel_phys_start: PhysAddr, + // kernel_phys_end: PhysAddr, + + // Init thread stack (will be reclaimed) + // init_stack_phys: PhysAddr, + // init_stack_size: usize, +} + +#[repr(C)] +struct MemoryRegion { + base: PhysAddr, + size: usize, + flags: MemoryFlags, +} + +#[repr(C)] +struct LoadedModule { + name: [u8; 32], + phys_start: PhysAddr, + size: usize, + entry_point: u64, // Offset from phys_start +} + +/// Static boot info - filled during early init +#[unsafe(link_section = ".init_thread.bss")] +static mut BOOT_INFO: BootInfo = BootInfo::zeroed(); diff --git a/libmemory/memory_test.rs b/libs/memory/test/memory_test.rs similarity index 100% rename from libmemory/memory_test.rs rename to libs/memory/test/memory_test.rs From ee74b279d550e2657ac418e610934a4708315028 Mon Sep 17 00:00:00 2001 From: Berkus Decker Date: Tue, 28 Jun 2022 12:57:07 +0300 Subject: [PATCH 037/107] wip: Add boot memory regions info --- kernel/init_thread/src/boot_info.rs | 130 ++++++++++++++++++++++++++++ 1 file changed, 130 insertions(+) create mode 100644 kernel/init_thread/src/boot_info.rs diff --git a/kernel/init_thread/src/boot_info.rs b/kernel/init_thread/src/boot_info.rs new file mode 100644 index 000000000..40b99f84a --- /dev/null +++ b/kernel/init_thread/src/boot_info.rs @@ -0,0 +1,130 @@ +use crate::{memory::PhysAddr, println, sync}; + +#[derive(Default, Copy, Clone)] +struct BootInfoMemRegion { + pub start: PhysAddr, + pub end: PhysAddr, +} + +impl BootInfoMemRegion { + pub const fn new() -> BootInfoMemRegion { + BootInfoMemRegion { + start: PhysAddr::zero(), + end: PhysAddr::zero(), + } + } + + pub fn size(&self) -> u64 { + self.end - self.start + } + + pub fn is_empty(&self) -> bool { + self.start == self.end + } +} + +const NUM_MEM_REGIONS: usize = 16; + +pub enum BootInfoError { + NoFreeMemRegions, +} + +#[derive(Default)] +struct BootInfo { + pub regions: [BootInfoMemRegion; NUM_MEM_REGIONS], + pub max_slot_pos: usize, +} + +impl BootInfo { + pub const fn new() -> BootInfo { + BootInfo { + regions: [BootInfoMemRegion::new(); NUM_MEM_REGIONS], + max_slot_pos: 0, + } + } + + pub fn insert_region(&mut self, reg: BootInfoMemRegion) -> Result<(), BootInfoError> { + if reg.is_empty() { + return Ok(()); + } + assert!(reg.start <= reg.end); + for region in self.regions.iter_mut() { + if region.is_empty() { + *region = reg; + return Ok(()); + } + } + return Err(BootInfoError::NoFreeMemRegions); + } + + pub fn alloc_region(&mut self, size_bits: usize) -> Result { + let mut reg_index: usize = 0; + let mut reg: BootInfoMemRegion = BootInfoMemRegion::new(); + let mut rem_small: BootInfoMemRegion = BootInfoMemRegion::new(); + let mut rem_large: BootInfoMemRegion = BootInfoMemRegion::new(); + /* + * Search for a free mem region that will be the best fit for an allocation. We favour allocations + * that are aligned to either end of the region. If an allocation must split a region we favour + * an unbalanced split. In both cases we attempt to use the smallest region possible. In general + * this means we aim to make the size of the smallest remaining region smaller (ideally zero) + * followed by making the size of the largest remaining region smaller. + */ + for (i, reg_iter) in self.regions.iter().enumerate() { + let mut new_reg: BootInfoMemRegion = BootInfoMemRegion::new(); + + /* Determine whether placing the region at the start or the end will create a bigger left over region */ + if reg_iter.start.aligned_up(1u64 << size_bits) - reg_iter.start + < reg_iter.end - reg_iter.end.aligned_down(1u64 << size_bits) + { + new_reg.start = reg_iter.start.aligned_up(1u64 << size_bits); + new_reg.end = new_reg.start + (1u64 << size_bits); + } else { + new_reg.end = reg_iter.end.aligned_down(1u64 << size_bits); + new_reg.start = new_reg.end - (1u64 << size_bits); + } + if new_reg.end > new_reg.start + && new_reg.start >= reg_iter.start + && new_reg.end <= reg_iter.end + { + let mut new_rem_small: BootInfoMemRegion = BootInfoMemRegion::new(); + let mut new_rem_large: BootInfoMemRegion = BootInfoMemRegion::new(); + + if new_reg.start - reg_iter.start < reg_iter.end - new_reg.end { + new_rem_small.start = reg_iter.start; + new_rem_small.end = new_reg.start; + new_rem_large.start = new_reg.end; + new_rem_large.end = reg_iter.end; + } else { + new_rem_large.start = reg_iter.start; + new_rem_large.end = new_reg.start; + new_rem_small.start = new_reg.end; + new_rem_small.end = reg_iter.end; + } + if reg.is_empty() + || (new_rem_small.size() < rem_small.size()) + || (new_rem_small.size() == rem_small.size() + && new_rem_large.size() < rem_large.size()) + { + reg = new_reg; + rem_small = new_rem_small; + rem_large = new_rem_large; + reg_index = i; + } + } + } + if reg.is_empty() { + panic!("Kernel init failed: not enough memory\n"); + } + /* Remove the region in question */ + self.regions[reg_index] = BootInfoMemRegion::new(); + /* Add the remaining regions in largest to smallest order */ + self.insert_region(rem_large)?; + if self.insert_region(rem_small).is_err() { + println!("BootInfo::alloc_region(): wasted {} bytes due to alignment, try to increase NUM_MEM_REGIONS", rem_small.size()); + } + Ok(reg.start) + } +} + +#[link_section = ".data.boot"] // @todo put zero-initialized stuff to .bss.boot! +static BOOT_INFO: sync::NullLock = sync::NullLock::new(BootInfo::new()); From a5f3b0c0749bead808e1771c28981eefb746d1cb Mon Sep 17 00:00:00 2001 From: Berkus Decker Date: Tue, 28 Jun 2022 13:00:44 +0300 Subject: [PATCH 038/107] wip: Switch to usize for alignment checks --- kernel/init_thread/src/boot_info.rs | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/kernel/init_thread/src/boot_info.rs b/kernel/init_thread/src/boot_info.rs index 40b99f84a..f7c68c007 100644 --- a/kernel/init_thread/src/boot_info.rs +++ b/kernel/init_thread/src/boot_info.rs @@ -73,14 +73,14 @@ impl BootInfo { let mut new_reg: BootInfoMemRegion = BootInfoMemRegion::new(); /* Determine whether placing the region at the start or the end will create a bigger left over region */ - if reg_iter.start.aligned_up(1u64 << size_bits) - reg_iter.start - < reg_iter.end - reg_iter.end.aligned_down(1u64 << size_bits) + if reg_iter.start.aligned_up(1usize << size_bits) - reg_iter.start + < reg_iter.end - reg_iter.end.aligned_down(1usize << size_bits) { - new_reg.start = reg_iter.start.aligned_up(1u64 << size_bits); - new_reg.end = new_reg.start + (1u64 << size_bits); + new_reg.start = reg_iter.start.aligned_up(1usize << size_bits); + new_reg.end = new_reg.start + (1usize << size_bits); } else { - new_reg.end = reg_iter.end.aligned_down(1u64 << size_bits); - new_reg.start = new_reg.end - (1u64 << size_bits); + new_reg.end = reg_iter.end.aligned_down(1usize << size_bits); + new_reg.start = new_reg.end - (1usize << size_bits); } if new_reg.end > new_reg.start && new_reg.start >= reg_iter.start @@ -120,7 +120,10 @@ impl BootInfo { /* Add the remaining regions in largest to smallest order */ self.insert_region(rem_large)?; if self.insert_region(rem_small).is_err() { - println!("BootInfo::alloc_region(): wasted {} bytes due to alignment, try to increase NUM_MEM_REGIONS", rem_small.size()); + println!( + "BootInfo::alloc_region(): wasted {} bytes due to alignment, try to increase NUM_MEM_REGIONS", + rem_small.size() + ); } Ok(reg.start) } From 225c464e2b44b46cafcb7bfc021e2aa1e96a3912 Mon Sep 17 00:00:00 2001 From: Berkus Decker Date: Wed, 4 Aug 2021 00:54:35 +0300 Subject: [PATCH 039/107] wip: messing with memregions --- kernel/init_thread/src/boot_info.rs | 161 ++++++++++++++++++++++++++-- 1 file changed, 151 insertions(+), 10 deletions(-) diff --git a/kernel/init_thread/src/boot_info.rs b/kernel/init_thread/src/boot_info.rs index f7c68c007..3e692330c 100644 --- a/kernel/init_thread/src/boot_info.rs +++ b/kernel/init_thread/src/boot_info.rs @@ -1,16 +1,90 @@ use crate::{memory::PhysAddr, println, sync}; +// @todo These are copied from memory/mod.rs Descriptor helper structs: + +/// Memory region attributes. +#[derive(Copy, Clone)] +pub enum MemAttributes { + /// Regular memory + CacheableDRAM, + /// Memory without caching + NonCacheableDRAM, + /// Device memory + Device, +} + +/// Memory region access permissions. +#[derive(Copy, Clone)] +pub enum AccessPermissions { + /// Read-only access + ReadOnly, + /// Read-write access + ReadWrite, +} + +/// Memory region translation. +#[allow(dead_code)] +#[derive(Copy, Clone)] +pub enum Translation { + /// One-to-one address mapping + Identity, + /// Mapping with a specified offset + Offset(usize), +} + +/// Summary structure of memory region properties. +#[derive(Copy, Clone)] +pub struct AttributeFields { + /// Attributes + pub mem_attributes: MemAttributes, + /// Permissions + pub acc_perms: AccessPermissions, + /// Disable executable code in this region + pub execute_never: bool, + /// This memory region is free + pub free: bool, +} + +impl Default for AttributeFields { + fn default() -> Self { + Self::defaulted() + } +} + +impl AttributeFields { + pub const fn defaulted() -> Self { + Self { + mem_attributes: MemAttributes::CacheableDRAM, + acc_perms: AccessPermissions::ReadWrite, + execute_never: true, + free: true, + } + } +} + +/// Memory region . #[derive(Default, Copy, Clone)] -struct BootInfoMemRegion { - pub start: PhysAddr, - pub end: PhysAddr, +pub struct BootInfoMemRegion { + pub start: PhysAddr, // start is inclusive + pub end: PhysAddr, // end is exclusive + pub attributes: AttributeFields, } impl BootInfoMemRegion { - pub const fn new() -> BootInfoMemRegion { - BootInfoMemRegion { + pub const fn new() -> Self { + Self { start: PhysAddr::zero(), end: PhysAddr::zero(), + attributes: AttributeFields::defaulted(), + } + } + + pub fn at(start: PhysAddr, end: PhysAddr, free: bool) -> Self { + use core::default::default; + Self { + start, + end, + attributes: AttributeFields { free, ..default() }, } } @@ -21,20 +95,40 @@ impl BootInfoMemRegion { pub fn is_empty(&self) -> bool { self.start == self.end } + + pub fn empty(&mut self) { + *self = Self::new(); + } + + pub fn intersects(&self, other: &BootInfoMemRegion) -> bool { + // https://eli.thegreenplace.net/2008/08/15/intersection-of-1d-segments/ + self.end >= other.start && other.end >= self.start + } } -const NUM_MEM_REGIONS: usize = 16; +const NUM_MEM_REGIONS: usize = 256; pub enum BootInfoError { NoFreeMemRegions, } -#[derive(Default)] -struct BootInfo { +pub struct BootInfo { pub regions: [BootInfoMemRegion; NUM_MEM_REGIONS], pub max_slot_pos: usize, } +// Implement Default manually to work around stupid Rust idea of not defining Default for arrays over 32 entries in size +impl Default for BootInfo { + fn default() -> Self { + Self::new() + } +} + +// @todo +// - use boot info to mark regions that are usable and not usable +// - build full memory map from the addresses we know and the DTB +// - print the derived memory layout + impl BootInfo { pub const fn new() -> BootInfo { BootInfo { @@ -43,6 +137,7 @@ impl BootInfo { } } + // Add a free memory region. pub fn insert_region(&mut self, reg: BootInfoMemRegion) -> Result<(), BootInfoError> { if reg.is_empty() { return Ok(()); @@ -57,6 +152,52 @@ impl BootInfo { return Err(BootInfoError::NoFreeMemRegions); } + // Remove a free memory region, turning it into allocated one. + // Different from alloc_region() in that we have a specific address and size to remove. + pub fn remove_region(&mut self, reg: BootInfoMemRegion) -> Result<(), BootInfoError> { + // Find intersection with existing regions. + // Subtracted region may intersect zero or more regions. + // Regions are not sorted in the list, so it may overlap any region at any point. + for (i, reg_iter) in self.regions.iter().enumerate() { + if reg_iter.start == reg.start { + // it may either cut off a piece from the start or completely eat the region + } else if reg.intersects(reg_iter) { + // they have common points, which must be resolved + /// it may intersect over the beginning of the region + if reg.start <= reg_iter.start && reg.end < reg_iter.end { + self.regions[i].start = reg.end; // end inclusive here? + } + /// it may intersect entirely inside the region, in which case we stop iterating + if reg.start > reg_iter.start && reg.end < reg_iter.end { + // split current region in two parts + let mut first_region = BootInfoMemRegion::at(reg_iter.start, reg.start, true); + let mut second_region = BootInfoMemRegion::at(reg.end, reg_iter.end, true); + self.regions[i].empty(); + if first_region.size() > second_region.size() { + self.insert_region(first_region); + return self.insert_region(second_region); + } else { + self.insert_region(second_region); + return self.insert_region(first_region); + } + } + /// it may intersect over the end of the region + if reg.start > reg_iter.start && reg.end > reg_iter.end { + self.regions[i].end = reg.start; + } + /// or it may entirely subsume the reg_iter + if reg.start <= reg_iter.start && reg.end >= reg_iter.end { + self.regions[i].empty(); + // it could also touch adjacent regions, so continue. + } + } else { + // no intersection and we can continue + } + } + Ok(()) + } + + // this method assumes all non-empty regions in BootInfo represent free memory pub fn alloc_region(&mut self, size_bits: usize) -> Result { let mut reg_index: usize = 0; let mut reg: BootInfoMemRegion = BootInfoMemRegion::new(); @@ -116,7 +257,7 @@ impl BootInfo { panic!("Kernel init failed: not enough memory\n"); } /* Remove the region in question */ - self.regions[reg_index] = BootInfoMemRegion::new(); + self.regions[reg_index].empty(); /* Add the remaining regions in largest to smallest order */ self.insert_region(rem_large)?; if self.insert_region(rem_small).is_err() { @@ -129,5 +270,5 @@ impl BootInfo { } } -#[link_section = ".data.boot"] // @todo put zero-initialized stuff to .bss.boot! +// Should go to BSS static BOOT_INFO: sync::NullLock = sync::NullLock::new(BootInfo::new()); From 4db647f54a3eea0c75843615c4d1543fde20136a Mon Sep 17 00:00:00 2001 From: Berkus Decker Date: Tue, 28 Jun 2022 13:07:19 +0300 Subject: [PATCH 040/107] wip: update BOOT_INFO struct with available mem info --- kernel/init_thread/src/boot_info.rs | 2 +- kernel/init_thread/src/main.rs | 36 +++++++++++++++++++++++------ 2 files changed, 30 insertions(+), 8 deletions(-) diff --git a/kernel/init_thread/src/boot_info.rs b/kernel/init_thread/src/boot_info.rs index 3e692330c..30e834c92 100644 --- a/kernel/init_thread/src/boot_info.rs +++ b/kernel/init_thread/src/boot_info.rs @@ -271,4 +271,4 @@ impl BootInfo { } // Should go to BSS -static BOOT_INFO: sync::NullLock = sync::NullLock::new(BootInfo::new()); +pub static BOOT_INFO: sync::NullLock = sync::NullLock::new(BootInfo::new()); diff --git a/kernel/init_thread/src/main.rs b/kernel/init_thread/src/main.rs index 5abde76eb..48bc62a54 100644 --- a/kernel/init_thread/src/main.rs +++ b/kernel/init_thread/src/main.rs @@ -69,11 +69,12 @@ fn panic(info: &PanicInfo) -> ! { libqemu::semihosting::exit_failure() } -// fn dump_memory_map() { -// Output the memory map as we could derive from FDT and information about our loaded image -// Use it to imagine how the memmap would look like in the end. -// arch::memory::print_layout(); -// } +fn dump_memory_map() { + // Output the memory map as we could derive from FDT and information about our loaded image + // Use it to imagine how the memmap would look like in the end. + // virt_mem_layout().print_layout(); + // TODO print bi.regions instead +} #[unsafe(no_mangle)] pub extern "C" fn init_main(dtb_ptr: *const u8) -> ! { @@ -171,6 +172,13 @@ pub extern "C" fn init_main(dtb_ptr: *const u8) -> ! { for (mem_addr, mem_size) in reg_prop.payload_pairs_iter() { semi_println!("Memory: {} KiB at offset {}", mem_size / 1024, mem_addr); + BOOT_INFO.lock(|bi| { + bi.insert_region(BootInfoMemRegion { + start: PhysAddr::new(mem_addr), + end: PhysAddr::new(mem_addr + mem_size), + attributes: default(), + }) + }); } // 4. List unusable memory, and remove it from the memory regions for the allocator. @@ -178,6 +186,13 @@ pub extern "C" fn init_main(dtb_ptr: *const u8) -> ! { let size: u64 = entry.size.into(); let address: u64 = entry.address.into(); semi_println!("Reserved memory: {size:?} bytes at {address:?}"); + BOOT_INFO.lock(|bi| { + bi.remove_region(BootInfoMemRegion::at( + PhysAddr::new(entry.address.into()), + PhysAddr::new(u64::from(entry.address) + u64::from(entry.size)), + false, + )) + }); } // 5. Also list memreserve entries, and remove then from allocator regions? @@ -195,8 +210,15 @@ pub extern "C" fn init_main(dtb_ptr: *const u8) -> ! { device_tree.fdt().totalsize(), dtb_ptr as usize ); // also include the raw_slice allocated bit - - // dump_memory_map(); + BOOT_INFO.lock(|bi| { + bi.remove_region(BootInfoMemRegion::at( + PhysAddr::new(dtb.into()), + PhysAddr::new(dtb as u64 + device_tree.fdt().totalsize() as u64), + false, + )) + }); + + dump_memory_map(); // Next step: parse DTB! // unsafe { From 3a3b8aafe317ce1adee6d078578b9d39871be549 Mon Sep 17 00:00:00 2001 From: Berkus Decker Date: Tue, 28 Jun 2022 13:08:29 +0300 Subject: [PATCH 041/107] wip: print some region info --- kernel/init_thread/src/boot_info.rs | 111 +++++++++++++++++++++++++--- kernel/init_thread/src/main.rs | 8 ++ 2 files changed, 110 insertions(+), 9 deletions(-) diff --git a/kernel/init_thread/src/boot_info.rs b/kernel/init_thread/src/boot_info.rs index 30e834c92..030e84c33 100644 --- a/kernel/init_thread/src/boot_info.rs +++ b/kernel/init_thread/src/boot_info.rs @@ -1,4 +1,7 @@ -use crate::{memory::PhysAddr, println, sync}; +use { + crate::{memory::PhysAddr, println, sync}, + core::fmt, +}; // @todo These are copied from memory/mod.rs Descriptor helper structs: @@ -62,8 +65,43 @@ impl AttributeFields { } } +impl fmt::Debug for MemAttributes { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let attr = match self { + MemAttributes::CacheableDRAM => "C", + MemAttributes::NonCacheableDRAM => "NC", + MemAttributes::Device => "Dev", + }; + write!(f, "{: <3}", attr); + Ok(()) + } +} + +impl fmt::Debug for AccessPermissions { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let acc_p = match self { + AccessPermissions::ReadOnly => "RO", + AccessPermissions::ReadWrite => "RW", + }; + write!(f, "{}", acc_p); + Ok(()) + } +} + +impl fmt::Debug for AttributeFields { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("AttributeFields") + .field("mem_attributes", &self.mem_attributes) + .field("acc_perms", &self.acc_perms) + .field("execute_never", &self.execute_never) + .field("free", &self.free) + .finish(); + Ok(()) + } +} + /// Memory region . -#[derive(Default, Copy, Clone)] +#[derive(Default, Copy, Clone, Debug)] pub struct BootInfoMemRegion { pub start: PhysAddr, // start is inclusive pub end: PhysAddr, // end is exclusive @@ -106,6 +144,55 @@ impl BootInfoMemRegion { } } +impl fmt::Display for BootInfoMemRegion { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + // log2(1024) + const KIB_RSHIFT: u32 = 10; + + // log2(1024 * 1024) + const MIB_RSHIFT: u32 = 20; + + let size = self.end - self.start; + + let (size, unit) = if (size >> MIB_RSHIFT) > 0 { + (size >> MIB_RSHIFT, "MiB") + } else if (size >> KIB_RSHIFT) > 0 { + (size >> KIB_RSHIFT, "KiB") + } else { + (size, "Bytes") + }; + + let attr = match self.attributes.mem_attributes { + MemAttributes::CacheableDRAM => "C", + MemAttributes::NonCacheableDRAM => "NC", + MemAttributes::Device => "Dev", + }; + + let acc_p = match self.attributes.acc_perms { + AccessPermissions::ReadOnly => "RO", + AccessPermissions::ReadWrite => "RW", + }; + + let xn = if self.attributes.execute_never { + "PXN" + } else { + "PX" + }; + + write!( + f, + " {:#010x} - {:#010x} | {: >3} {} | {: <3} {} {: <3}", // | {}", + self.start, + self.end, + size, + unit, + attr, + acc_p, + xn, //self.name + ) + } +} + const NUM_MEM_REGIONS: usize = 256; pub enum BootInfoError { @@ -152,27 +239,33 @@ impl BootInfo { return Err(BootInfoError::NoFreeMemRegions); } - // Remove a free memory region, turning it into allocated one. + // Remove a free memory region, turning it into an allocated one. // Different from alloc_region() in that we have a specific address and size to remove. pub fn remove_region(&mut self, reg: BootInfoMemRegion) -> Result<(), BootInfoError> { // Find intersection with existing regions. // Subtracted region may intersect zero or more regions. // Regions are not sorted in the list, so it may overlap any region at any point. - for (i, reg_iter) in self.regions.iter().enumerate() { + for reg_iter in self.regions.iter_mut() { if reg_iter.start == reg.start { // it may either cut off a piece from the start or completely eat the region + if reg.end >= reg_iter.end { + reg_iter.empty(); + } else { + reg_iter.start = reg.end; + return Ok(()); + } } else if reg.intersects(reg_iter) { // they have common points, which must be resolved /// it may intersect over the beginning of the region if reg.start <= reg_iter.start && reg.end < reg_iter.end { - self.regions[i].start = reg.end; // end inclusive here? + reg_iter.start = reg.end; // end inclusive here? } /// it may intersect entirely inside the region, in which case we stop iterating if reg.start > reg_iter.start && reg.end < reg_iter.end { // split current region in two parts let mut first_region = BootInfoMemRegion::at(reg_iter.start, reg.start, true); let mut second_region = BootInfoMemRegion::at(reg.end, reg_iter.end, true); - self.regions[i].empty(); + reg_iter.empty(); if first_region.size() > second_region.size() { self.insert_region(first_region); return self.insert_region(second_region); @@ -182,12 +275,12 @@ impl BootInfo { } } /// it may intersect over the end of the region - if reg.start > reg_iter.start && reg.end > reg_iter.end { - self.regions[i].end = reg.start; + if reg.start > reg_iter.start && reg.end >= reg_iter.end { + reg_iter.end = reg.start; } /// or it may entirely subsume the reg_iter if reg.start <= reg_iter.start && reg.end >= reg_iter.end { - self.regions[i].empty(); + reg_iter.empty(); // it could also touch adjacent regions, so continue. } } else { diff --git a/kernel/init_thread/src/main.rs b/kernel/init_thread/src/main.rs index 48bc62a54..f1d857162 100644 --- a/kernel/init_thread/src/main.rs +++ b/kernel/init_thread/src/main.rs @@ -220,6 +220,14 @@ pub extern "C" fn init_main(dtb_ptr: *const u8) -> ! { dump_memory_map(); + BOOT_INFO.lock(|bi| { + for x in bi.regions { + if !x.is_empty() { + println!("{}", x); + } + } + }); + // Next step: parse DTB! // unsafe { // BOOT_INFO.dtb_size = dtb.total_size(); From ca8b8f75e69dd49655f5b5d3a10b0fc1535e01bb Mon Sep 17 00:00:00 2001 From: Berkus Decker Date: Thu, 30 Jun 2022 01:46:03 +0300 Subject: [PATCH 042/107] wip: refactor and fix compile --- kernel/init_thread/src/boot_info.rs | 61 ++++++++++++----------------- kernel/init_thread/src/main.rs | 4 ++ kernel/init_thread/src/memory.rs | 10 +++++ 3 files changed, 40 insertions(+), 35 deletions(-) diff --git a/kernel/init_thread/src/boot_info.rs b/kernel/init_thread/src/boot_info.rs index 030e84c33..16a09316e 100644 --- a/kernel/init_thread/src/boot_info.rs +++ b/kernel/init_thread/src/boot_info.rs @@ -1,6 +1,8 @@ use { - crate::{memory::PhysAddr, println, sync}, + crate::{arch::memory::PhysAddr, println, sync}, core::fmt, + once_cell::unsync::Lazy, + snafu::Snafu, }; // @todo These are copied from memory/mod.rs Descriptor helper structs: @@ -25,16 +27,6 @@ pub enum AccessPermissions { ReadWrite, } -/// Memory region translation. -#[allow(dead_code)] -#[derive(Copy, Clone)] -pub enum Translation { - /// One-to-one address mapping - Identity, - /// Mapping with a specified offset - Offset(usize), -} - /// Summary structure of memory region properties. #[derive(Copy, Clone)] pub struct AttributeFields { @@ -72,8 +64,7 @@ impl fmt::Debug for MemAttributes { MemAttributes::NonCacheableDRAM => "NC", MemAttributes::Device => "Dev", }; - write!(f, "{: <3}", attr); - Ok(()) + write!(f, "{: <3}", attr) } } @@ -83,8 +74,7 @@ impl fmt::Debug for AccessPermissions { AccessPermissions::ReadOnly => "RO", AccessPermissions::ReadWrite => "RW", }; - write!(f, "{}", acc_p); - Ok(()) + write!(f, "{}", acc_p) } } @@ -95,12 +85,11 @@ impl fmt::Debug for AttributeFields { .field("acc_perms", &self.acc_perms) .field("execute_never", &self.execute_never) .field("free", &self.free) - .finish(); - Ok(()) + .finish() } } -/// Memory region . +/// Memory region. #[derive(Default, Copy, Clone, Debug)] pub struct BootInfoMemRegion { pub start: PhysAddr, // start is inclusive @@ -181,7 +170,7 @@ impl fmt::Display for BootInfoMemRegion { write!( f, - " {:#010x} - {:#010x} | {: >3} {} | {: <3} {} {: <3}", // | {}", + " {:#010X} - {:#010X} | {: >3} {} | {: <3} {} {: <3}", // | {}", self.start, self.end, size, @@ -195,6 +184,7 @@ impl fmt::Display for BootInfoMemRegion { const NUM_MEM_REGIONS: usize = 256; +#[derive(Snafu, Debug)] pub enum BootInfoError { NoFreeMemRegions, } @@ -256,29 +246,29 @@ impl BootInfo { } } else if reg.intersects(reg_iter) { // they have common points, which must be resolved - /// it may intersect over the beginning of the region + // it may intersect over the beginning of the region if reg.start <= reg_iter.start && reg.end < reg_iter.end { reg_iter.start = reg.end; // end inclusive here? } - /// it may intersect entirely inside the region, in which case we stop iterating + // it may intersect entirely inside the region, in which case we stop iterating if reg.start > reg_iter.start && reg.end < reg_iter.end { // split current region in two parts - let mut first_region = BootInfoMemRegion::at(reg_iter.start, reg.start, true); - let mut second_region = BootInfoMemRegion::at(reg.end, reg_iter.end, true); + let first_region = BootInfoMemRegion::at(reg_iter.start, reg.start, true); + let second_region = BootInfoMemRegion::at(reg.end, reg_iter.end, true); reg_iter.empty(); if first_region.size() > second_region.size() { - self.insert_region(first_region); + self.insert_region(first_region)?; return self.insert_region(second_region); } else { - self.insert_region(second_region); + self.insert_region(second_region)?; return self.insert_region(first_region); } } - /// it may intersect over the end of the region + // it may intersect over the end of the region if reg.start > reg_iter.start && reg.end >= reg_iter.end { reg_iter.end = reg.start; } - /// or it may entirely subsume the reg_iter + // or it may entirely subsume the reg_iter if reg.start <= reg_iter.start && reg.end >= reg_iter.end { reg_iter.empty(); // it could also touch adjacent regions, so continue. @@ -307,14 +297,14 @@ impl BootInfo { let mut new_reg: BootInfoMemRegion = BootInfoMemRegion::new(); /* Determine whether placing the region at the start or the end will create a bigger left over region */ - if reg_iter.start.aligned_up(1usize << size_bits) - reg_iter.start - < reg_iter.end - reg_iter.end.aligned_down(1usize << size_bits) + if reg_iter.start.aligned_up(1u64 << size_bits) - reg_iter.start + < reg_iter.end - reg_iter.end.aligned_down(1u64 << size_bits) { - new_reg.start = reg_iter.start.aligned_up(1usize << size_bits); - new_reg.end = new_reg.start + (1usize << size_bits); + new_reg.start = reg_iter.start.aligned_up(1u64 << size_bits); + new_reg.end = new_reg.start + (1u64 << size_bits); } else { - new_reg.end = reg_iter.end.aligned_down(1usize << size_bits); - new_reg.start = new_reg.end - (1usize << size_bits); + new_reg.end = reg_iter.end.aligned_down(1u64 << size_bits); + new_reg.start = new_reg.end - (1u64 << size_bits); } if new_reg.end > new_reg.start && new_reg.start >= reg_iter.start @@ -350,7 +340,7 @@ impl BootInfo { panic!("Kernel init failed: not enough memory\n"); } /* Remove the region in question */ - self.regions[reg_index].empty(); + self.regions[reg_index] = BootInfoMemRegion::new(); /* Add the remaining regions in largest to smallest order */ self.insert_region(rem_large)?; if self.insert_region(rem_small).is_err() { @@ -364,4 +354,5 @@ impl BootInfo { } // Should go to BSS -pub static BOOT_INFO: sync::NullLock = sync::NullLock::new(BootInfo::new()); +pub static BOOT_INFO: sync::NullLock> = + sync::NullLock::new(Lazy::new(|| BootInfo::new())); diff --git a/kernel/init_thread/src/main.rs b/kernel/init_thread/src/main.rs index f1d857162..97678e5d6 100644 --- a/kernel/init_thread/src/main.rs +++ b/kernel/init_thread/src/main.rs @@ -35,6 +35,7 @@ // - scheduler (invokes process upcall key) mod boot; +mod boot_info; mod device_tree; mod el_switch; mod embed; @@ -178,6 +179,7 @@ pub extern "C" fn init_main(dtb_ptr: *const u8) -> ! { end: PhysAddr::new(mem_addr + mem_size), attributes: default(), }) + .expect("tough luck"); }); } @@ -192,6 +194,7 @@ pub extern "C" fn init_main(dtb_ptr: *const u8) -> ! { PhysAddr::new(u64::from(entry.address) + u64::from(entry.size)), false, )) + .expect("tough luck"); }); } @@ -216,6 +219,7 @@ pub extern "C" fn init_main(dtb_ptr: *const u8) -> ! { PhysAddr::new(dtb as u64 + device_tree.fdt().totalsize() as u64), false, )) + .expect("tough luck"); }); dump_memory_map(); diff --git a/kernel/init_thread/src/memory.rs b/kernel/init_thread/src/memory.rs index a421ec26b..cfdc72458 100644 --- a/kernel/init_thread/src/memory.rs +++ b/kernel/init_thread/src/memory.rs @@ -2,6 +2,16 @@ use crate::loader::LoadableSection; +/// Memory region translation. +// #[allow(dead_code)] +// #[derive(Copy, Clone)] +// pub enum Translation { +// /// One-to-one address mapping +// Identity, +// /// Mapping with a specified offset +// Offset(usize), +// } + #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] #[repr(transparent)] pub struct PhysAddr(pub u64); From 3d85ba669ae00e1e143d580e59291e3bbd23fce5 Mon Sep 17 00:00:00 2001 From: Berkus Decker Date: Sun, 3 Jul 2022 11:24:48 +0300 Subject: [PATCH 043/107] wip: make bootinfo bss-initializable by default wip: bss --- kernel/init_thread/src/boot_info.rs | 31 ++++++++++++++++------------- 1 file changed, 17 insertions(+), 14 deletions(-) diff --git a/kernel/init_thread/src/boot_info.rs b/kernel/init_thread/src/boot_info.rs index 16a09316e..501f0db9c 100644 --- a/kernel/init_thread/src/boot_info.rs +++ b/kernel/init_thread/src/boot_info.rs @@ -21,10 +21,10 @@ pub enum MemAttributes { /// Memory region access permissions. #[derive(Copy, Clone)] pub enum AccessPermissions { - /// Read-only access - ReadOnly, /// Read-write access ReadWrite, + /// Read-only access + ReadOnly, } /// Summary structure of memory region properties. @@ -34,10 +34,10 @@ pub struct AttributeFields { pub mem_attributes: MemAttributes, /// Permissions pub acc_perms: AccessPermissions, - /// Disable executable code in this region - pub execute_never: bool, - /// This memory region is free - pub free: bool, + /// Allow executable code in this region + pub executable: bool, + /// This memory region is occupied + pub occupied: bool, } impl Default for AttributeFields { @@ -51,8 +51,8 @@ impl AttributeFields { Self { mem_attributes: MemAttributes::CacheableDRAM, acc_perms: AccessPermissions::ReadWrite, - execute_never: true, - free: true, + executable: false, + occupied: false, } } } @@ -83,8 +83,8 @@ impl fmt::Debug for AttributeFields { f.debug_struct("AttributeFields") .field("mem_attributes", &self.mem_attributes) .field("acc_perms", &self.acc_perms) - .field("execute_never", &self.execute_never) - .field("free", &self.free) + .field("executable", &self.executable) + .field("occupied", &self.occupied) .finish() } } @@ -111,7 +111,10 @@ impl BootInfoMemRegion { Self { start, end, - attributes: AttributeFields { free, ..default() }, + attributes: AttributeFields { + occupied: !free, + ..default() + }, } } @@ -162,10 +165,10 @@ impl fmt::Display for BootInfoMemRegion { AccessPermissions::ReadWrite => "RW", }; - let xn = if self.attributes.execute_never { - "PXN" - } else { + let xn = if self.attributes.executable { "PX" + } else { + "PXN" }; write!( From 13f7c5cbe68918fff8e0c5c181cfc8e5c39604da Mon Sep 17 00:00:00 2001 From: Berkus Decker Date: Sun, 3 Jul 2022 11:24:58 +0300 Subject: [PATCH 044/107] wip: add InvalidRegion error code --- kernel/init_thread/src/boot_info.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/kernel/init_thread/src/boot_info.rs b/kernel/init_thread/src/boot_info.rs index 501f0db9c..a19912f17 100644 --- a/kernel/init_thread/src/boot_info.rs +++ b/kernel/init_thread/src/boot_info.rs @@ -190,6 +190,7 @@ const NUM_MEM_REGIONS: usize = 256; #[derive(Snafu, Debug)] pub enum BootInfoError { NoFreeMemRegions, + InvalidRegion, } pub struct BootInfo { @@ -222,7 +223,9 @@ impl BootInfo { if reg.is_empty() { return Ok(()); } - assert!(reg.start <= reg.end); + if reg.start > reg.end { + return Err(BootInfoError::InvalidRegion); + } for region in self.regions.iter_mut() { if region.is_empty() { *region = reg; From df0e6e09fa3259f2e62ccee248983b0841fd06d7 Mon Sep 17 00:00:00 2001 From: Berkus Decker Date: Sun, 10 Jul 2022 16:43:35 +0300 Subject: [PATCH 045/107] wip: enable testing of dependency crates (not working yet) wip: revert test feature --- kernel/init_thread/src/boot_info.rs | 14 ++++++++++++++ libs/test/src/lib.rs | 2 +- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/kernel/init_thread/src/boot_info.rs b/kernel/init_thread/src/boot_info.rs index a19912f17..02a4a8393 100644 --- a/kernel/init_thread/src/boot_info.rs +++ b/kernel/init_thread/src/boot_info.rs @@ -362,3 +362,17 @@ impl BootInfo { // Should go to BSS pub static BOOT_INFO: sync::NullLock> = sync::NullLock::new(Lazy::new(|| BootInfo::new())); + +#[cfg(test)] +mod boot_info_tests { + use super::*; + + #[test_case] + fn test_add_invalid_region() { + let mut bi = BootInfo::new(); + let region = BootInfoMemRegion::at(0x2000.into(), 0x0.into(), true); + let res = bi.insert_region(region); + assert!(res.is_err()); + assert_eq!(res.err(), Some(BootInfoError::InvalidRegion)); + } +} diff --git a/libs/test/src/lib.rs b/libs/test/src/lib.rs index e474a826a..cba92e4d2 100644 --- a/libs/test/src/lib.rs +++ b/libs/test/src/lib.rs @@ -27,7 +27,7 @@ where } pub fn test_runner(tests: &[&dyn TestFn]) { - liblog::println!("Running {} tests", tests.len()); + liblog::println!("*TESTING* Running {} tests", tests.len()); for test in tests { test.run(); } From 8e53ade03e00aafb79f66eec4836456672f96ea0 Mon Sep 17 00:00:00 2001 From: Berkus Decker Date: Sun, 17 Jul 2022 00:46:11 +0300 Subject: [PATCH 046/107] wip: add docs and stuff --- kernel/init_thread/src/boot_info.rs | 217 ++++++++++++++++++++-------- kernel/init_thread/src/main.rs | 4 +- 2 files changed, 159 insertions(+), 62 deletions(-) diff --git a/kernel/init_thread/src/boot_info.rs b/kernel/init_thread/src/boot_info.rs index 02a4a8393..58af16a05 100644 --- a/kernel/init_thread/src/boot_info.rs +++ b/kernel/init_thread/src/boot_info.rs @@ -1,3 +1,6 @@ +//! Boot regions +//! +//! Define a map of memory regions used during boot allocations. use { crate::{arch::memory::PhysAddr, println, sync}, core::fmt, @@ -47,6 +50,7 @@ impl Default for AttributeFields { } impl AttributeFields { + /// Create zero-initialized attribute structure. pub const fn defaulted() -> Self { Self { mem_attributes: MemAttributes::CacheableDRAM, @@ -89,50 +93,68 @@ impl fmt::Debug for AttributeFields { } } +//================================================================================================= +// BootInfoMemRegion +//================================================================================================= + /// Memory region. #[derive(Default, Copy, Clone, Debug)] pub struct BootInfoMemRegion { - pub start: PhysAddr, // start is inclusive - pub end: PhysAddr, // end is exclusive + /// Region start is inclusive. + pub start_inclusive: PhysAddr, + /// Region end is exclusive. + pub end_exclusive: PhysAddr, pub attributes: AttributeFields, } impl BootInfoMemRegion { + /// Create an empty region. pub const fn new() -> Self { Self { - start: PhysAddr::zero(), - end: PhysAddr::zero(), + start_inclusive: PhysAddr::zero(), + end_exclusive: PhysAddr::zero(), attributes: AttributeFields::defaulted(), } } - pub fn at(start: PhysAddr, end: PhysAddr, free: bool) -> Self { - use core::default::default; + /// Create an occupied or free region with start and end. + /// Region is in range [start, end), that is, for start 0x0 and end 0x2000 the region will + /// occupy memory between addresses 0x0 and 0x1fff. + pub fn at(start_inclusive: PhysAddr, end_exclusive: PhysAddr, free: bool) -> Self { Self { - start, - end, + start_inclusive: start_inclusive.min(end_exclusive), + end_exclusive: end_exclusive.max(start_inclusive), attributes: AttributeFields { occupied: !free, - ..default() + ..core::default::default() }, } } + /// Calculate region size. pub fn size(&self) -> u64 { - self.end - self.start + self.end_exclusive - self.start_inclusive } + /// Is this region empty? pub fn is_empty(&self) -> bool { - self.start == self.end + self.start_inclusive == self.end_exclusive } - pub fn empty(&mut self) { + /// Clear the region to empty. + pub fn clear(&mut self) { *self = Self::new(); } + /// Does this region intersect the given one? + /// Based on [Intersection of 1D segments](https://eli.thegreenplace.net/2008/08/15/intersection-of-1d-segments/). + /// + /// Since end is exclusive, the actual value is one less than what it contains, for this reason, + /// end equal to other's start means they touch but do not intersect. + /// + /// Assumes start_inclusive <= end_exclusive, which holds for memory regions by construction. pub fn intersects(&self, other: &BootInfoMemRegion) -> bool { - // https://eli.thegreenplace.net/2008/08/15/intersection-of-1d-segments/ - self.end >= other.start && other.end >= self.start + self.end_exclusive > other.start_inclusive && other.end_exclusive > self.start_inclusive } } @@ -144,14 +166,14 @@ impl fmt::Display for BootInfoMemRegion { // log2(1024 * 1024) const MIB_RSHIFT: u32 = 20; - let size = self.end - self.start; + let size = self.size(); let (size, unit) = if (size >> MIB_RSHIFT) > 0 { (size >> MIB_RSHIFT, "MiB") } else if (size >> KIB_RSHIFT) > 0 { (size >> KIB_RSHIFT, "KiB") } else { - (size, "Bytes") + (size, "B") }; let attr = match self.attributes.mem_attributes { @@ -173,9 +195,9 @@ impl fmt::Display for BootInfoMemRegion { write!( f, - " {:#010X} - {:#010X} | {: >3} {} | {: <3} {} {: <3}", // | {}", - self.start, - self.end, + " [{:#010X} - {:#010X}) | {: >3} {} | {: <3} {} {: <3}", // | {}", + self.start_inclusive, + self.end_exclusive, size, unit, attr, @@ -185,6 +207,60 @@ impl fmt::Display for BootInfoMemRegion { } } +#[cfg(test)] +mod boot_info_region_tests { + use super::*; + + #[test_case] + fn test_construct_regular_region() { + let region = BootInfoMemRegion::at(0x0.into(), 0x2000.into(), true); + assert_eq!(region.start_inclusive, 0x0); + assert_eq!(region.end_exclusive, 0x2000); + assert_eq!(region.size(), 0x2000); + assert_eq!(region.attributes.occupied, false); + } + + #[test_case] + fn test_construct_reverse_region() { + let region = BootInfoMemRegion::at(0x2000.into(), 0x0.into(), true); + assert_eq!(region.start_inclusive, 0x0); + assert_eq!(region.end_exclusive, 0x2000); + assert_eq!(region.size(), 0x2000); + assert_eq!(region.attributes.occupied, false); + } + + #[test_case] + fn test_regions_touch() { + let region1 = BootInfoMemRegion::at(0x0.into(), 0x2000.into(), false); + let region2 = BootInfoMemRegion::at(0x2000.into(), 0x4000.into(), false); + assert_eq!(region1.intersects(®ion2), false); + assert_eq!(region2.intersects(®ion1), false); + } + + #[test_case] + fn test_regions_intersect() { + let region1 = BootInfoMemRegion::at(0x0.into(), 0x2000.into(), false); + let region2 = BootInfoMemRegion::at(0x1000.into(), 0x3000.into(), false); + assert_eq!(region1.intersects(®ion2), true); + assert_eq!(region2.intersects(®ion1), true); + } + + #[test_case] + fn test_self_intersect() { + let region1 = BootInfoMemRegion::at(0x0.into(), 0x2000.into(), false); + let region1 = BootInfoMemRegion::at(0x0.into(), 0x2000.into(), false); + assert_eq!(region1.intersects(®ion2), true); + assert_eq!(region2.intersects(®ion1), true); + } + + #[test_case] + fn test_regions_fully_overlap() {} +} + +//================================================================================================= +// BootInfo +//================================================================================================= + const NUM_MEM_REGIONS: usize = 256; #[derive(Snafu, Debug)] @@ -198,7 +274,7 @@ pub struct BootInfo { pub max_slot_pos: usize, } -// Implement Default manually to work around stupid Rust idea of not defining Default for arrays over 32 entries in size +/// Implement Default manually to work around stupid Rust idea of not defining Default for arrays over 32 entries in size impl Default for BootInfo { fn default() -> Self { Self::new() @@ -211,6 +287,7 @@ impl Default for BootInfo { // - print the derived memory layout impl BootInfo { + /// Create empty boot region map. pub const fn new() -> BootInfo { BootInfo { regions: [BootInfoMemRegion::new(); NUM_MEM_REGIONS], @@ -218,12 +295,12 @@ impl BootInfo { } } - // Add a free memory region. + /// Add a free memory region. pub fn insert_region(&mut self, reg: BootInfoMemRegion) -> Result<(), BootInfoError> { if reg.is_empty() { return Ok(()); } - if reg.start > reg.end { + if reg.start_inclusive > reg.end_exclusive { return Err(BootInfoError::InvalidRegion); } for region in self.regions.iter_mut() { @@ -235,33 +312,46 @@ impl BootInfo { return Err(BootInfoError::NoFreeMemRegions); } - // Remove a free memory region, turning it into an allocated one. - // Different from alloc_region() in that we have a specific address and size to remove. - pub fn remove_region(&mut self, reg: BootInfoMemRegion) -> Result<(), BootInfoError> { + /// Remove a free memory region, turning it into an allocated one. + /// Different from alloc_region() in that we have a specific address and size to remove. + pub fn remove_region(&mut self, remove_region: BootInfoMemRegion) -> Result<(), BootInfoError> { // Find intersection with existing regions. // Subtracted region may intersect zero or more regions. // Regions are not sorted in the list, so it may overlap any region at any point. - for reg_iter in self.regions.iter_mut() { - if reg_iter.start == reg.start { + for iterated_region in self.regions.iter_mut() { + if iterated_region.start_inclusive == remove_region.start_inclusive { // it may either cut off a piece from the start or completely eat the region - if reg.end >= reg_iter.end { - reg_iter.empty(); + if remove_region.end_exclusive >= iterated_region.end_exclusive { + iterated_region.clear(); } else { - reg_iter.start = reg.end; + iterated_region.start_inclusive = remove_region.end_exclusive; return Ok(()); } - } else if reg.intersects(reg_iter) { + } else if remove_region.intersects(iterated_region) { // they have common points, which must be resolved // it may intersect over the beginning of the region - if reg.start <= reg_iter.start && reg.end < reg_iter.end { - reg_iter.start = reg.end; // end inclusive here? + if remove_region.start_inclusive <= iterated_region.start_inclusive + && remove_region.end_exclusive < iterated_region.end_exclusive + { + iterated_region.start_inclusive = remove_region.end_exclusive; + // @todo end inclusive here? } // it may intersect entirely inside the region, in which case we stop iterating - if reg.start > reg_iter.start && reg.end < reg_iter.end { + if remove_region.start_inclusive > iterated_region.start_inclusive + && remove_region.end_exclusive < iterated_region.end_exclusive + { // split current region in two parts - let first_region = BootInfoMemRegion::at(reg_iter.start, reg.start, true); - let second_region = BootInfoMemRegion::at(reg.end, reg_iter.end, true); - reg_iter.empty(); + let first_region = BootInfoMemRegion::at( + iterated_region.start_inclusive, + remove_region.start_inclusive, + true, + ); + let second_region = BootInfoMemRegion::at( + remove_region.end_exclusive, + iterated_region.end_exclusive, + true, + ); + iterated_region.clear(); if first_region.size() > second_region.size() { self.insert_region(first_region)?; return self.insert_region(second_region); @@ -271,12 +361,16 @@ impl BootInfo { } } // it may intersect over the end of the region - if reg.start > reg_iter.start && reg.end >= reg_iter.end { - reg_iter.end = reg.start; + if remove_region.start_inclusive > iterated_region.start_inclusive + && remove_region.end_exclusive >= iterated_region.end_exclusive + { + iterated_region.end_exclusive = remove_region.start_inclusive; } // or it may entirely subsume the reg_iter - if reg.start <= reg_iter.start && reg.end >= reg_iter.end { - reg_iter.empty(); + if remove_region.start_inclusive <= iterated_region.start_inclusive + && remove_region.end_exclusive >= iterated_region.end_exclusive + { + iterated_region.clear(); // it could also touch adjacent regions, so continue. } } else { @@ -303,32 +397,34 @@ impl BootInfo { let mut new_reg: BootInfoMemRegion = BootInfoMemRegion::new(); /* Determine whether placing the region at the start or the end will create a bigger left over region */ - if reg_iter.start.aligned_up(1u64 << size_bits) - reg_iter.start - < reg_iter.end - reg_iter.end.aligned_down(1u64 << size_bits) + if reg_iter.start_inclusive.aligned_up(1u64 << size_bits) - reg_iter.start_inclusive + < reg_iter.end_exclusive - reg_iter.end_exclusive.aligned_down(1u64 << size_bits) { - new_reg.start = reg_iter.start.aligned_up(1u64 << size_bits); - new_reg.end = new_reg.start + (1u64 << size_bits); + new_reg.start_inclusive = reg_iter.start_inclusive.aligned_up(1u64 << size_bits); + new_reg.end_exclusive = new_reg.start_inclusive + (1u64 << size_bits); } else { - new_reg.end = reg_iter.end.aligned_down(1u64 << size_bits); - new_reg.start = new_reg.end - (1u64 << size_bits); + new_reg.end_exclusive = reg_iter.end_exclusive.aligned_down(1u64 << size_bits); + new_reg.start_inclusive = new_reg.end_exclusive - (1u64 << size_bits); } - if new_reg.end > new_reg.start - && new_reg.start >= reg_iter.start - && new_reg.end <= reg_iter.end + if new_reg.end_exclusive > new_reg.start_inclusive + && new_reg.start_inclusive >= reg_iter.start_inclusive + && new_reg.end_exclusive <= reg_iter.end_exclusive { let mut new_rem_small: BootInfoMemRegion = BootInfoMemRegion::new(); let mut new_rem_large: BootInfoMemRegion = BootInfoMemRegion::new(); - if new_reg.start - reg_iter.start < reg_iter.end - new_reg.end { - new_rem_small.start = reg_iter.start; - new_rem_small.end = new_reg.start; - new_rem_large.start = new_reg.end; - new_rem_large.end = reg_iter.end; + if new_reg.start_inclusive - reg_iter.start_inclusive + < reg_iter.end_exclusive - new_reg.end_exclusive + { + new_rem_small.start_inclusive = reg_iter.start_inclusive; + new_rem_small.end_exclusive = new_reg.start_inclusive; + new_rem_large.start_inclusive = new_reg.end_exclusive; + new_rem_large.end_exclusive = reg_iter.end_exclusive; } else { - new_rem_large.start = reg_iter.start; - new_rem_large.end = new_reg.start; - new_rem_small.start = new_reg.end; - new_rem_small.end = reg_iter.end; + new_rem_large.start_inclusive = reg_iter.start_inclusive; + new_rem_large.end_exclusive = new_reg.start_inclusive; + new_rem_small.start_inclusive = new_reg.end_exclusive; + new_rem_small.end_exclusive = reg_iter.end_exclusive; } if reg.is_empty() || (new_rem_small.size() < rem_small.size()) @@ -347,6 +443,7 @@ impl BootInfo { } /* Remove the region in question */ self.regions[reg_index] = BootInfoMemRegion::new(); + // self.regions[reg_index].clear(); /* Add the remaining regions in largest to smallest order */ self.insert_region(rem_large)?; if self.insert_region(rem_small).is_err() { @@ -355,7 +452,7 @@ impl BootInfo { rem_small.size() ); } - Ok(reg.start) + Ok(reg.start_inclusive) } } diff --git a/kernel/init_thread/src/main.rs b/kernel/init_thread/src/main.rs index 97678e5d6..42cf2b2f6 100644 --- a/kernel/init_thread/src/main.rs +++ b/kernel/init_thread/src/main.rs @@ -175,8 +175,8 @@ pub extern "C" fn init_main(dtb_ptr: *const u8) -> ! { semi_println!("Memory: {} KiB at offset {}", mem_size / 1024, mem_addr); BOOT_INFO.lock(|bi| { bi.insert_region(BootInfoMemRegion { - start: PhysAddr::new(mem_addr), - end: PhysAddr::new(mem_addr + mem_size), + start_inclusive: PhysAddr::new(mem_addr), + end_exclusive: PhysAddr::new(mem_addr + mem_size), attributes: default(), }) .expect("tough luck"); From 6aa951d0ed88bb0fab2129d7da6e10d775430ff2 Mon Sep 17 00:00:00 2001 From: Berkus Decker Date: Sun, 17 Jul 2022 02:51:31 +0300 Subject: [PATCH 047/107] wip: refactor boot_info blocks --- kernel/init_thread/src/boot_info.rs | 61 ++++++++++++++--------------- 1 file changed, 30 insertions(+), 31 deletions(-) diff --git a/kernel/init_thread/src/boot_info.rs b/kernel/init_thread/src/boot_info.rs index 58af16a05..e838e924f 100644 --- a/kernel/init_thread/src/boot_info.rs +++ b/kernel/init_thread/src/boot_info.rs @@ -296,16 +296,13 @@ impl BootInfo { } /// Add a free memory region. - pub fn insert_region(&mut self, reg: BootInfoMemRegion) -> Result<(), BootInfoError> { - if reg.is_empty() { + pub fn insert_region(&mut self, new_region: BootInfoMemRegion) -> Result<(), BootInfoError> { + if new_region.is_empty() { return Ok(()); } - if reg.start_inclusive > reg.end_exclusive { - return Err(BootInfoError::InvalidRegion); - } for region in self.regions.iter_mut() { if region.is_empty() { - *region = reg; + *region = new_region; return Ok(()); } } @@ -352,13 +349,13 @@ impl BootInfo { true, ); iterated_region.clear(); - if first_region.size() > second_region.size() { + return if first_region.size() > second_region.size() { self.insert_region(first_region)?; - return self.insert_region(second_region); + self.insert_region(second_region) } else { self.insert_region(second_region)?; - return self.insert_region(first_region); - } + self.insert_region(first_region) + }; } // it may intersect over the end of the region if remove_region.start_inclusive > iterated_region.start_inclusive @@ -380,34 +377,34 @@ impl BootInfo { Ok(()) } - // this method assumes all non-empty regions in BootInfo represent free memory + /// Allocate a region of given `size_bits` size. + /// + /// Search for a free mem region that will be the best fit for an allocation. We favour allocations + /// that are aligned to either end of the region. If an allocation must split a region we favour + /// an unbalanced split. In both cases we attempt to use the smallest region possible. In general + /// this means we aim to make the size of the smallest remaining region smaller (ideally zero) + /// followed by making the size of the largest remaining region smaller. + /// + // @fixme this method assumes all non-empty regions in BootInfo represent free memory pub fn alloc_region(&mut self, size_bits: usize) -> Result { let mut reg_index: usize = 0; let mut reg: BootInfoMemRegion = BootInfoMemRegion::new(); let mut rem_small: BootInfoMemRegion = BootInfoMemRegion::new(); let mut rem_large: BootInfoMemRegion = BootInfoMemRegion::new(); - /* - * Search for a free mem region that will be the best fit for an allocation. We favour allocations - * that are aligned to either end of the region. If an allocation must split a region we favour - * an unbalanced split. In both cases we attempt to use the smallest region possible. In general - * this means we aim to make the size of the smallest remaining region smaller (ideally zero) - * followed by making the size of the largest remaining region smaller. - */ - for (i, reg_iter) in self.regions.iter().enumerate() { - let mut new_reg: BootInfoMemRegion = BootInfoMemRegion::new(); - /* Determine whether placing the region at the start or the end will create a bigger left over region */ - if reg_iter.start_inclusive.aligned_up(1u64 << size_bits) - reg_iter.start_inclusive - < reg_iter.end_exclusive - reg_iter.end_exclusive.aligned_down(1u64 << size_bits) + for (i, reg_iter) in self.regions.iter().enumerate() { + // Determine whether placing the region at the start or the end will create a bigger left over region. + let aligned_start = reg_iter.start_inclusive.aligned_up(1u64 << size_bits); + let aligned_end = reg_iter.end_exclusive.aligned_down(1u64 << size_bits); + let new_reg = if aligned_start - reg_iter.start_inclusive + < reg_iter.end_exclusive - aligned_end { - new_reg.start_inclusive = reg_iter.start_inclusive.aligned_up(1u64 << size_bits); - new_reg.end_exclusive = new_reg.start_inclusive + (1u64 << size_bits); + BootInfoMemRegion::at(aligned_start, aligned_start + (1u64 << size_bits), false) } else { - new_reg.end_exclusive = reg_iter.end_exclusive.aligned_down(1u64 << size_bits); - new_reg.start_inclusive = new_reg.end_exclusive - (1u64 << size_bits); - } - if new_reg.end_exclusive > new_reg.start_inclusive - && new_reg.start_inclusive >= reg_iter.start_inclusive + BootInfoMemRegion::at(aligned_end - (1u64 << size_bits), aligned_end, false) + }; + + if new_reg.start_inclusive >= reg_iter.start_inclusive && new_reg.end_exclusive <= reg_iter.end_exclusive { let mut new_rem_small: BootInfoMemRegion = BootInfoMemRegion::new(); @@ -426,6 +423,7 @@ impl BootInfo { new_rem_small.start_inclusive = new_reg.end_exclusive; new_rem_small.end_exclusive = reg_iter.end_exclusive; } + // Find better fit. if reg.is_empty() || (new_rem_small.size() < rem_small.size()) || (new_rem_small.size() == rem_small.size() @@ -439,6 +437,7 @@ impl BootInfo { } } if reg.is_empty() { + // @fixme just return error! panic!("Kernel init failed: not enough memory\n"); } /* Remove the region in question */ @@ -470,6 +469,6 @@ mod boot_info_tests { let region = BootInfoMemRegion::at(0x2000.into(), 0x0.into(), true); let res = bi.insert_region(region); assert!(res.is_err()); - assert_eq!(res.err(), Some(BootInfoError::InvalidRegion)); + assert_eq!(res.err(), Some(BootInfoError::InvalidRegion)); // @fixme not thrown anymore } } From 03f33278f09e4642aa2795c2e9d3e936f88a4744 Mon Sep 17 00:00:00 2001 From: Berkus Decker Date: Tue, 27 Jan 2026 00:43:21 +0200 Subject: [PATCH 048/107] =?UTF-8?q?wip:=20boot=5Finfo=20=E2=AC=87=EF=B8=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- kernel/init_thread/src/boot_info.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kernel/init_thread/src/boot_info.rs b/kernel/init_thread/src/boot_info.rs index e838e924f..89ecb69ad 100644 --- a/kernel/init_thread/src/boot_info.rs +++ b/kernel/init_thread/src/boot_info.rs @@ -2,7 +2,7 @@ //! //! Define a map of memory regions used during boot allocations. use { - crate::{arch::memory::PhysAddr, println, sync}, + crate::{memory::PhysAddr, println, sync}, core::fmt, once_cell::unsync::Lazy, snafu::Snafu, From 269cac36b1978d98b5d99596d23cb5173dac46c6 Mon Sep 17 00:00:00 2001 From: Berkus Decker Date: Tue, 27 Jan 2026 01:52:11 +0200 Subject: [PATCH 049/107] wip: compile fixes --- kernel/init_thread/Cargo.toml | 1 + kernel/init_thread/src/boot_info.rs | 20 ++++---- kernel/init_thread/src/loader.rs | 3 +- kernel/init_thread/src/main.rs | 13 ++++-- kernel/init_thread/src/memory.rs | 71 ++++------------------------- kernel/init_thread/src/paging.rs | 15 +++--- kernel/nucleus/nucleus.ld | 2 +- libs/memory/src/phys_addr.rs | 10 +++- libs/memory/src/virt_addr.rs | 6 ++- 9 files changed, 52 insertions(+), 89 deletions(-) diff --git a/kernel/init_thread/Cargo.toml b/kernel/init_thread/Cargo.toml index bab259243..bc763ca27 100644 --- a/kernel/init_thread/Cargo.toml +++ b/kernel/init_thread/Cargo.toml @@ -38,6 +38,7 @@ libcpu = { workspace = true } libexception = { workspace = true } libkernel-state = { workspace = true } liblog = { workspace = true } +liblocking = { workspace = true } libmachine = { workspace = true } libmemory = { workspace = true } libplatform = { workspace = true } diff --git a/kernel/init_thread/src/boot_info.rs b/kernel/init_thread/src/boot_info.rs index 89ecb69ad..63b26b3ad 100644 --- a/kernel/init_thread/src/boot_info.rs +++ b/kernel/init_thread/src/boot_info.rs @@ -2,9 +2,9 @@ //! //! Define a map of memory regions used during boot allocations. use { - crate::{memory::PhysAddr, println, sync}, - core::fmt, - once_cell::unsync::Lazy, + core::{cell::LazyCell, fmt}, + liblocking::IRQSafeNullLock, + libmemory::phys_addr::PhysAddr, snafu::Snafu, }; @@ -126,7 +126,7 @@ impl BootInfoMemRegion { end_exclusive: end_exclusive.max(start_inclusive), attributes: AttributeFields { occupied: !free, - ..core::default::default() + ..AttributeFields::default() }, } } @@ -195,7 +195,7 @@ impl fmt::Display for BootInfoMemRegion { write!( f, - " [{:#010X} - {:#010X}) | {: >3} {} | {: <3} {} {: <3}", // | {}", + " [{:#010x} - {:#010x}) | {: >3} {} | {: <3} {} {: <3}", // | {}", self.start_inclusive, self.end_exclusive, size, @@ -394,8 +394,8 @@ impl BootInfo { for (i, reg_iter) in self.regions.iter().enumerate() { // Determine whether placing the region at the start or the end will create a bigger left over region. - let aligned_start = reg_iter.start_inclusive.aligned_up(1u64 << size_bits); - let aligned_end = reg_iter.end_exclusive.aligned_down(1u64 << size_bits); + let aligned_start = reg_iter.start_inclusive.aligned_up(1usize << size_bits); + let aligned_end = reg_iter.end_exclusive.aligned_down(1usize << size_bits); let new_reg = if aligned_start - reg_iter.start_inclusive < reg_iter.end_exclusive - aligned_end { @@ -446,7 +446,7 @@ impl BootInfo { /* Add the remaining regions in largest to smallest order */ self.insert_region(rem_large)?; if self.insert_region(rem_small).is_err() { - println!( + libqemu::semi_println!( "BootInfo::alloc_region(): wasted {} bytes due to alignment, try to increase NUM_MEM_REGIONS", rem_small.size() ); @@ -456,8 +456,8 @@ impl BootInfo { } // Should go to BSS -pub static BOOT_INFO: sync::NullLock> = - sync::NullLock::new(Lazy::new(|| BootInfo::new())); +pub static BOOT_INFO: IRQSafeNullLock> = + IRQSafeNullLock::new(LazyCell::new(|| BootInfo::new())); #[cfg(test)] mod boot_info_tests { diff --git a/kernel/init_thread/src/loader.rs b/kernel/init_thread/src/loader.rs index 88868f3f9..48d3b4e06 100644 --- a/kernel/init_thread/src/loader.rs +++ b/kernel/init_thread/src/loader.rs @@ -3,9 +3,10 @@ use { crate::{ embed::KERNEL, - memory::{BootAllocator, KernelLayout, MemoryPermissions, PhysAddr, VirtAddr}, + memory::{BootAllocator, KernelLayout, MemoryPermissions}, }, core::ptr, + libmemory::{phys_addr::PhysAddr, virt_addr::VirtAddr}, libqemu::semi_println, }; diff --git a/kernel/init_thread/src/main.rs b/kernel/init_thread/src/main.rs index 42cf2b2f6..0ea6d076c 100644 --- a/kernel/init_thread/src/main.rs +++ b/kernel/init_thread/src/main.rs @@ -45,6 +45,7 @@ mod paging; mod syscall_test; use { + crate::boot_info::{BOOT_INFO, BootInfoMemRegion}, core::{panic::PanicInfo, ptr::write_bytes, slice}, device_tree::{DeviceTree, DeviceTreeProp}, fdt_rs::{ @@ -53,8 +54,10 @@ use { prelude::{FallibleIterator, PropReader}, }, libcpu::endless_sleep, + liblocking::interface::Mutex, + libmemory::{phys_addr::PhysAddr, virt_addr::VirtAddr}, libqemu::semi_println, - memory::{BootAllocator, PhysAddr}, + memory::BootAllocator, syscall_test::protected_call6, }; @@ -177,7 +180,7 @@ pub extern "C" fn init_main(dtb_ptr: *const u8) -> ! { bi.insert_region(BootInfoMemRegion { start_inclusive: PhysAddr::new(mem_addr), end_exclusive: PhysAddr::new(mem_addr + mem_size), - attributes: default(), + attributes: boot_info::AttributeFields::default(), }) .expect("tough luck"); }); @@ -215,8 +218,8 @@ pub extern "C" fn init_main(dtb_ptr: *const u8) -> ! { ); // also include the raw_slice allocated bit BOOT_INFO.lock(|bi| { bi.remove_region(BootInfoMemRegion::at( - PhysAddr::new(dtb.into()), - PhysAddr::new(dtb as u64 + device_tree.fdt().totalsize() as u64), + PhysAddr::new(dtb_ptr as u64), + PhysAddr::new(dtb_ptr as u64 + device_tree.fdt().totalsize() as u64), false, )) .expect("tough luck"); @@ -227,7 +230,7 @@ pub extern "C" fn init_main(dtb_ptr: *const u8) -> ! { BOOT_INFO.lock(|bi| { for x in bi.regions { if !x.is_empty() { - println!("{}", x); + semi_println!("{}", x); } } }); diff --git a/kernel/init_thread/src/memory.rs b/kernel/init_thread/src/memory.rs index cfdc72458..6491600e0 100644 --- a/kernel/init_thread/src/memory.rs +++ b/kernel/init_thread/src/memory.rs @@ -1,6 +1,9 @@ // Boot allocator, Section mapping, memory permissions -use crate::loader::LoadableSection; +use { + crate::loader::LoadableSection, + libmemory::{phys_addr::PhysAddr, virt_addr::VirtAddr}, +}; /// Memory region translation. // #[allow(dead_code)] @@ -12,72 +15,16 @@ use crate::loader::LoadableSection; // Offset(usize), // } -#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] -#[repr(transparent)] -pub struct PhysAddr(pub u64); - -#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] -#[repr(transparent)] -pub struct VirtAddr(pub u64); - -impl PhysAddr { - pub const fn new(addr: u64) -> Self { - Self(addr) - } - pub const fn as_u64(self) -> u64 { - self.0 - } - pub const fn as_ptr(self) -> *const T { - self.0 as *const T - } - pub fn as_mut_ptr(self) -> *mut T { - self.0 as *mut T - } - pub const fn add(self, offset: u64) -> Self { - Self(self.0 + offset) - } - pub const fn align_up(self, align: u64) -> Self { - Self((self.0 + align - 1) & !(align - 1)) - } - pub const fn align_down(self, align: u64) -> Self { - Self(self.0 & !(align - 1)) - } - pub const fn is_aligned(self, align: u64) -> bool { - self.0 & (align - 1) == 0 - } -} - -impl VirtAddr { - pub const fn new(addr: u64) -> Self { - Self(addr) - } - pub const fn as_u64(self) -> u64 { - self.0 - } - pub const fn add(self, offset: u64) -> Self { - Self(self.0 + offset) - } - pub const fn sub(self, other: VirtAddr) -> u64 { - self.0 - other.0 - } - pub const fn is_higher_half(self) -> bool { - self.0 >= 0xFFFF_0000_0000_0000 - } - pub const fn is_aligned(self, align: u64) -> bool { - self.0 & (align - 1) == 0 - } -} - pub struct BootAllocator { current: PhysAddr, end: PhysAddr, } impl BootAllocator { - pub const fn new(start: PhysAddr, size: usize) -> Self { + pub fn new(start: PhysAddr, size: usize) -> Self { Self { current: start, - end: PhysAddr(start.0 + size as u64), + end: PhysAddr::new(start.0 + size as u64), } } @@ -86,8 +33,8 @@ impl BootAllocator { } pub fn alloc_aligned(&mut self, size: usize, align: usize) -> Option { - let aligned = self.current.align_up(align as u64); - let new_current = PhysAddr(aligned.0 + size as u64); + let aligned = self.current.aligned_up(align); + let new_current = PhysAddr::new(aligned.0 + size as u64); libqemu::semi_println!( "alloc_aligned {:#016x} => {:#016x} (wrt {:#016x})", @@ -186,7 +133,7 @@ pub struct KernelLayout { impl KernelLayout { pub fn virt_to_phys(&self, virt: VirtAddr) -> PhysAddr { let offset = virt.as_u64() - self.virt_base.as_u64(); - PhysAddr(self.phys_base.as_u64() + offset) + PhysAddr::new(self.phys_base.as_u64() + offset) } /// Get the VBAR_EL1 value (virtual address for use after MMU enable) diff --git a/kernel/init_thread/src/paging.rs b/kernel/init_thread/src/paging.rs index d2b8e0583..1858bf48e 100644 --- a/kernel/init_thread/src/paging.rs +++ b/kernel/init_thread/src/paging.rs @@ -17,16 +17,15 @@ /// 0xFFFF_0000_0000_0000 └─────────────────────┘ /// ``` use { - crate::memory::{ - BootAllocator, KernelLayout, MemoryPermissions, PhysAddr, SectionMapping, VirtAddr, - }, + crate::memory::{BootAllocator, KernelLayout, MemoryPermissions, SectionMapping}, core::ptr, + libmemory::{phys_addr::PhysAddr, virt_addr::VirtAddr}, libqemu::semi_println, }; -const KERNEL_BASE: u64 = 0xFFFF_0000_0000_0000; -const KERNEL_PHYS_MAP: u64 = 0xFFFF_0000_0000_0000; // Linear map base -const KERNEL_DEVICE_BASE: u64 = 0xFFFF_0080_0000_0000; +const KERNEL_BASE: u64 = 0xFFFF_8000_0000_0000; +const KERNEL_PHYS_MAP: u64 = 0xFFFF_8000_0000_0000; // Linear map base +const KERNEL_DEVICE_BASE: u64 = 0xFFFF_8080_0000_0000; const KERNEL_DCB_BASE: u64 = 0xFFFF_FF00_0000_0000; const KERNEL_HEAP_BASE: u64 = 0xFFFF_FFFF_0000_0000; const KERNEL_STACK_BASE: u64 = 0xFFFF_FFFF_8000_0000; @@ -244,8 +243,8 @@ pub fn create_identity_mapping( start: PhysAddr, end: PhysAddr, ) -> Result<(), &'static str> { - let start_aligned = start.align_down(2 * 1024 * 1024); - let end_aligned = end.align_up(2 * 1024 * 1024); + let start_aligned = start.aligned_down(2 * 1024 * 1024); + let end_aligned = end.aligned_up(2 * 1024 * 1024); let perms = MemoryPermissions { readable: true, diff --git a/kernel/nucleus/nucleus.ld b/kernel/nucleus/nucleus.ld index 91442e763..5c361d9ec 100644 --- a/kernel/nucleus/nucleus.ld +++ b/kernel/nucleus/nucleus.ld @@ -8,7 +8,7 @@ __PAGE_SIZE = 64K; __PAGE_MASK = __PAGE_SIZE - 1; -__KERNEL_VIRT_ADDR_BASE = 0xffff000000000000; /* Nucleus is mapped here, init_thread is identity-mapped */ +__KERNEL_VIRT_ADDR_BASE = 0xffff800000000000; /* Nucleus is mapped here, init_thread is identity-mapped */ /* Flags: * 4 == R diff --git a/libs/memory/src/phys_addr.rs b/libs/memory/src/phys_addr.rs index aa294fd4c..7484aa8da 100644 --- a/libs/memory/src/phys_addr.rs +++ b/libs/memory/src/phys_addr.rs @@ -27,7 +27,7 @@ use { /// to be zero. This type guarantees that it always represents a valid physical address. #[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Default)] #[repr(transparent)] -pub struct PhysAddr(u64); +pub struct PhysAddr(pub u64); /// A passed `u64` was not a valid physical address. /// @@ -68,6 +68,14 @@ impl PhysAddr { self.0 } + pub const fn as_ptr(self) -> *const T { + self.0 as *const T + } + + pub fn as_mut_ptr(self) -> *mut T { + self.0 as *mut T + } + /// Convenience method for checking if a physical address is null. pub fn is_null(&self) -> bool { self.0 == 0 diff --git a/libs/memory/src/virt_addr.rs b/libs/memory/src/virt_addr.rs index 29b863c84..ba5b89052 100644 --- a/libs/memory/src/virt_addr.rs +++ b/libs/memory/src/virt_addr.rs @@ -29,7 +29,7 @@ use { /// are called “canonical”. This type guarantees that it always represents a canonical address. #[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Default)] #[repr(transparent)] -pub struct VirtAddr(u64); +pub struct VirtAddr(pub u64); /// A passed `u64` was not a valid virtual address. /// @@ -163,6 +163,10 @@ impl VirtAddr { u9::new(((self.0 >> 12 >> 9 >> 9 >> 9) & 0o777).try_into().unwrap()) } + pub const fn is_higher_half(self) -> bool { + self.0 >= 0xFFFF_0000_0000_0000 + } + /// Convert kernel-view virtual address of physical memory into a physical memory address. pub fn kernel_to_user(&self) -> PhysAddr { use super::PHYSICAL_KERNEL_WINDOW; From 3b3624809bfb5fbbb28883f3e5bca7ea7d141563 Mon Sep 17 00:00:00 2001 From: Berkus Decker Date: Thu, 29 Jan 2026 16:48:57 +0200 Subject: [PATCH 050/107] wip: add Key types and simplest DebugConsole capability invocation --- Cargo.toml | 2 + kernel/init_thread/Cargo.toml | 1 + kernel/init_thread/src/syscall_test.rs | 44 ------ kernel/nucleus/Cargo.toml | 1 + kernel/nucleus/src/api/buffer.rs | 2 +- kernel/nucleus/src/api/debug_console.rs | 26 +++- kernel/nucleus/src/api/key_entry.rs | 5 + kernel/nucleus/src/api/key_table.rs | 140 +++++++++++++++++- kernel/nucleus/src/api/mod.rs | 24 ++- kernel/nucleus/src/main.rs | 21 +-- libs/syscall/Cargo.toml | 24 +++ .../api/syscall.rs => libs/syscall/src/lib.rs | 139 ++++++++++++----- 12 files changed, 309 insertions(+), 120 deletions(-) delete mode 100644 kernel/init_thread/src/syscall_test.rs create mode 100644 kernel/nucleus/src/api/key_entry.rs create mode 100644 libs/syscall/Cargo.toml rename kernel/nucleus/src/api/syscall.rs => libs/syscall/src/lib.rs (64%) diff --git a/Cargo.toml b/Cargo.toml index 417832361..1990eb370 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,6 +20,7 @@ members = [ "libs/primitives", "libs/print", "libs/qemu", + "libs/syscall", "libs/test", "libs/time", ] @@ -61,6 +62,7 @@ libplatform = { path = "libs/platform" } libprimitives = { path = "libs/primitives" } libprint = { path = "libs/print" } libqemu = { path = "libs/qemu" } +libsyscall = { path = "libs/syscall" } libtest = { path = "libs/test" } libtime = { path = "libs/time" } num = { version = "0.4", default-features = false } diff --git a/kernel/init_thread/Cargo.toml b/kernel/init_thread/Cargo.toml index bc763ca27..0bb5b9cab 100644 --- a/kernel/init_thread/Cargo.toml +++ b/kernel/init_thread/Cargo.toml @@ -44,6 +44,7 @@ libmemory = { workspace = true } libplatform = { workspace = true } libprint = { workspace = true } libqemu = { workspace = true, optional = true } +libsyscall = { workspace = true } libtime = { workspace = true } snafu = { workspace = true } static_assertions = { workspace = true } diff --git a/kernel/init_thread/src/syscall_test.rs b/kernel/init_thread/src/syscall_test.rs deleted file mode 100644 index 5193bdc2f..000000000 --- a/kernel/init_thread/src/syscall_test.rs +++ /dev/null @@ -1,44 +0,0 @@ -/// Single syscall ABI -/// -/// Entry: SVC #0 -/// -/// Arguments: -/// x0 = capability slot -/// x1 = operation code -/// x2-x7 = operation arguments (6 args!) -/// x9-x15 are caller-saved, we don't use them -/// -/// Returns: -/// x0 = error code (0 = success) -/// x1 = return value 0 -/// x2 = return value 1 (if needed) -#[inline(always)] -pub unsafe fn protected_call6( - cap: u32, - op: u32, - a0: u64, - a1: u64, - a2: u64, - a3: u64, - a4: u64, - a5: u64, -) -> (u64, u64, u64) { - let r0: u64; - let r1: u64; - let r2: u64; - unsafe { - core::arch::asm!( - "svc #0", - inlateout("x0") cap as u64 => r0, - inlateout("x1") op as u64 => r1, - inlateout("x2") a0 => r2, - in("x3") a1, - in("x4") a2, - in("x5") a3, - in("x6") a4, - in("x7") a5, - options(nostack), - ); - } - (r0, r1, r2) -} diff --git a/kernel/nucleus/Cargo.toml b/kernel/nucleus/Cargo.toml index 96303884a..8a1396852 100644 --- a/kernel/nucleus/Cargo.toml +++ b/kernel/nucleus/Cargo.toml @@ -46,6 +46,7 @@ libmemory = { workspace = true } libplatform = { workspace = true } libprint = { workspace = true } libqemu = { workspace = true, optional = true } +libsyscall = { workspace = true } libtime = { workspace = true } snafu = { workspace = true } static_assertions = { workspace = true } diff --git a/kernel/nucleus/src/api/buffer.rs b/kernel/nucleus/src/api/buffer.rs index 81f041ed3..75c65a920 100644 --- a/kernel/nucleus/src/api/buffer.rs +++ b/kernel/nucleus/src/api/buffer.rs @@ -224,7 +224,7 @@ enum BufferOp { // == Syscall handler == // ===================== -pub fn invoke(cap: &CapEntry, op: u32, arg0: u64, arg1: u64) -> SyscallResult { +pub fn invoke(cap: &KeyEntry, op: u32, arg0: u64, arg1: u64) -> SyscallResult { let buffer = cap.as_buffer()?; let caller = current_domain(); diff --git a/kernel/nucleus/src/api/debug_console.rs b/kernel/nucleus/src/api/debug_console.rs index a560ec8fa..f9e573d35 100644 --- a/kernel/nucleus/src/api/debug_console.rs +++ b/kernel/nucleus/src/api/debug_console.rs @@ -1,7 +1,12 @@ +use { + crate::api::key::Key, + core::slice, + libsyscall::{SyscallError, SyscallResult, protected_call2}, +}; + // ================================================== // == Public user interface, usable from userspace == // ================================================== - pub struct DebugConsoleKey { key: Key, } @@ -10,11 +15,12 @@ pub struct DebugConsoleKey { impl DebugConsoleKey { pub fn write(&self, s: &str) -> Result<()> { protected_call2( - self.slot, + self.key.slot, DebugConsoleOp::Write as u32, s.as_ptr() as u64, s.len() as u64, - ) + )?; + Ok(()) } } @@ -25,7 +31,14 @@ impl DebugConsoleKey { struct DebugConsole; impl DebugConsole { - fn handle_write() {} + fn handle_write(ptr: u64, len: u64) { + let slice = slice::from_raw_parts(ptr, len as usize); + let buf = [0u8; 4096]; + buf.copy_from_slice(slice); + buf[slice.len()] = 0; + let cstr = unsafe { core::ffi::CStr::from_bytes_with_nul(&buf[..=slice.len() + 1]) }; + libqemu::semihosting::sys_write0_call(cstr); + } } #[repr(u8)] @@ -37,8 +50,11 @@ pub enum DebugConsoleOp { // == Syscall handler == // ===================== -pub fn invoke(cap: &CapEntry, op: u32, arg0: u64, arg1: u64) -> SyscallResult { +pub fn invoke(cap: &KeyEntry, op: u32, arg0: u64, arg1: u64) -> SyscallResult { + let console = cap.as_debug_console()?; match op { + DebugConsoleOp::Write => console.handle_write(arg0, arg1), _ => Err(SyscallError::InvalidOp), } + Ok((0, 0)) } diff --git a/kernel/nucleus/src/api/key_entry.rs b/kernel/nucleus/src/api/key_entry.rs new file mode 100644 index 000000000..1c0581f0c --- /dev/null +++ b/kernel/nucleus/src/api/key_entry.rs @@ -0,0 +1,5 @@ +// Trait-like wrapper around kernel objects + +impl KeyEntry { + pub fn as_debug_console() -> Result {} +} diff --git a/kernel/nucleus/src/api/key_table.rs b/kernel/nucleus/src/api/key_table.rs index 8cd4bd1dd..47211f493 100644 --- a/kernel/nucleus/src/api/key_table.rs +++ b/kernel/nucleus/src/api/key_table.rs @@ -1,12 +1,12 @@ -// ================================================== -// == Public user interface, usable from userspace == -// ================================================== - use crate::{ SyscallError, api::{protected_call1, protected_call4}, }; +// ================================================== +// == Public user interface, usable from userspace == +// ================================================== + pub struct KeyTableKey { key: Key, } @@ -133,8 +133,134 @@ pub fn invoke(key: &Key, op: u32, args: &[u64]) -> SyscallResult { // Copy cap from this captbl[src] to dst_captbl[dst_slot] //... } - KeyTableOp::Move => {} - KeyTableOp::Delete => {} - KeyTableOp::Revoke => {} + KeyTableOp::Move => { + // Copy cap from captbl[src] to dst_captbl[dst_slot] + // Delete cap in captbl[src] + } + KeyTableOp::Delete => { + // Delete cap in captbl[src] + } + KeyTableOp::Revoke => { + // Revoke cap, by bumping it's epoch and making derived accesses invalid + } + } +} + +////========== +////========== +////========== +////========== +////========== + +// ═══════════════════════════════════════════════════════════════════ +// KEY TABLE (CAPABILITY TABLE / CNODE) +// ═══════════════════════════════════════════════════════════════════ + +/// Slot index in a KeyTable +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +#[repr(transparent)] +pub struct KeySlot(pub u16); + +impl KeySlot { + pub const NULL: KeySlot = KeySlot(0); + pub const SELF_DOMAIN: KeySlot = KeySlot(1); + pub const PARENT_DOMAIN: KeySlot = KeySlot(2); + // ... other well-known slots +} + +/// A capability table for a domain. +/// +/// This is what seL4 calls a CNode. Each domain has one. +/// The table itself is a kernel object that can be referenced +/// by capabilities (for capability space manipulation). +pub struct KeyTable { + /// The actual capability entries + entries: [KeyEntry; Self::NUM_SLOTS], + /// Domain that owns this table + owner: DomainId, + /// Number of valid entries (for iteration) + count: u16, +} + +impl KeyTable { + /// Number of slots per table (power of 2 for fast indexing) + pub const NUM_SLOTS: usize = 256; + + /// Create a new empty capability table + pub fn new(owner: DomainId) -> Self { + Self { + entries: [const { KeyEntry::null() }; Self::NUM_SLOTS], + owner, + count: 0, + } + } + + /// Lookup a capability by slot index + #[inline] + pub fn lookup(&self, slot: KeySlot) -> Result<&KeyEntry, CapError> { + let idx = slot.0 as usize; + if idx >= Self::NUM_SLOTS { + return Err(CapError::InvalidSlot(slot)); + } + + let entry = &self.entries[idx]; + if !entry.is_valid() { + return Err(CapError::EmptySlot(slot)); + } + + Ok(entry) } + + /// Lookup a capability mutably + #[inline] + pub fn lookup_mut(&mut self, slot: KeySlot) -> Result<&mut KeyEntry, CapError> { + let idx = slot.0 as usize; + if idx >= Self::NUM_SLOTS { + return Err(CapError::InvalidSlot(slot)); + } + + let entry = &mut self.entries[idx]; + if !entry.is_valid() { + return Err(CapError::EmptySlot(slot)); + } + + Ok(entry) + } + + /// Insert a capability at a specific slot + pub fn insert(&mut self, slot: KeySlot, entry: KeyEntry) -> Result<(), CapError> { + let idx = slot.0 as usize; + if idx >= Self::NUM_SLOTS { + return Err(CapError::InvalidSlot(slot)); + } + + if self.entries[idx].is_valid() { + return Err(CapError::SlotOccupied(slot)); + } + + self.entries[idx] = entry; + self.count += 1; + Ok(()) + } + + /// Remove a capability from a slot + pub fn remove(&mut self, slot: KeySlot) -> Result { + let idx = slot.0 as usize; + if idx >= Self::NUM_SLOTS { + return Err(CapError::InvalidSlot(slot)); + } + + let entry = core::mem::replace(&mut self.entries[idx], KeyEntry::null()); + if entry.is_valid() { + self.count -= 1; + Ok(entry) + } else { + Err(CapError::EmptySlot(slot)) + } + } +} + +// KeyTable is itself a kernel object +impl KernelObject for KeyTable { + const TYPE: ObjectType = ObjectType::KeyTable; } diff --git a/kernel/nucleus/src/api/mod.rs b/kernel/nucleus/src/api/mod.rs index 38e61fcd4..4afe42730 100644 --- a/kernel/nucleus/src/api/mod.rs +++ b/kernel/nucleus/src/api/mod.rs @@ -1,20 +1,14 @@ // TODO: move these api decls/impls to libraries in libs/, re-export only user part through // TODO: libsyscall or something like that - usable from userspace. -mod buffer; +// mod buffer; mod debug_console; -mod domain; -mod endpoint; -mod event_count; +// mod domain; +// mod endpoint; +// mod event_count; mod key; -mod key_table; -mod notification; -mod reply; -mod syscall; -mod time; -mod untyped; - -pub use syscall::{ - protected_call0, protected_call1, protected_call2, protected_call3, protected_call4, - protected_call5, protected_call6, -}; +// mod key_table; +// mod notification; +// mod reply; +// mod time; +// mod untyped; diff --git a/kernel/nucleus/src/main.rs b/kernel/nucleus/src/main.rs index 03dc78310..96c6f971d 100644 --- a/kernel/nucleus/src/main.rs +++ b/kernel/nucleus/src/main.rs @@ -27,10 +27,11 @@ use { core::{arch::asm, cell::UnsafeCell, panic::PanicInfo, time::Duration}, libcpu::endless_sleep, liblog::{info, println, warn}, + libmemory::mmu::AccessPermissions, libqemu::semi_println, }; -// mod api; +mod api; mod vectors; #[panic_handler] @@ -64,8 +65,6 @@ fn panicked(info: &PanicInfo) -> ! { // endpoint::EndpointOp, // }; -struct Nucleus; - /// Capability types now include: pub enum ObjectType { /// No capability @@ -88,17 +87,6 @@ pub enum ObjectType { Buffer = 8, } -type SyscallResult = Result<(u64, u64), SyscallError>; - -enum SyscallError { - PermissionDenied, - InvalidOp, - SlotOccupied, - AlreadyMapped, - NotMapped, - InvalidPointer, -} - // Syscall handler - exception vector for EL0 synchronous exceptions // (the only other thing nucleus does) #[unsafe(naked)] @@ -167,7 +155,10 @@ fn cap_invoke_handler( arg5: u64, frame: u64, //*mut TrapFrame, ) -> (u64, u64, u64) { - semi_println!("CapInvoke SYSCALL happened, we're at 0x{:016X}", get_pc()); + semi_println!( + "CapInvoke SYSCALL(cap: {cap_slot}, op: {op}) happened, we're at 0x{:016X}", + get_pc() + ); return (0, 0, 0); // let cap = current_domain().keytable.lookup(cap_slot)?; diff --git a/libs/syscall/Cargo.toml b/libs/syscall/Cargo.toml new file mode 100644 index 000000000..0db8ab4a8 --- /dev/null +++ b/libs/syscall/Cargo.toml @@ -0,0 +1,24 @@ +[package] +name = "libsyscall" + +description = "Syscall interface" + +authors = { workspace = true } +categories = { workspace = true } +documentation = { workspace = true } +edition = { workspace = true } +homepage = { workspace = true } +license = { workspace = true } +readme = { workspace = true } +repository = { workspace = true } +version = { workspace = true } + +publish = false + +[badges] +maintenance = { status = "experimental" } + +[dependencies] + +[lints] +workspace = true diff --git a/kernel/nucleus/src/api/syscall.rs b/libs/syscall/src/lib.rs similarity index 64% rename from kernel/nucleus/src/api/syscall.rs rename to libs/syscall/src/lib.rs index 71af17952..3e39ced39 100644 --- a/kernel/nucleus/src/api/syscall.rs +++ b/libs/syscall/src/lib.rs @@ -1,3 +1,7 @@ +#![no_std] + +use core::result::Result; + // Syscall ABI: // ┌─────────────────────────────────────────────────────────────────────────┐ // │ CAPTBL COPY (cross-domain grant): │ @@ -29,6 +33,32 @@ // │ └─────────────────────────────────────────────────────────────────┘ │ // └─────────────────────────────────────────────────────────────────────────┘ +pub type SyscallResult = Result<(u64, u64), SyscallError>; + +pub enum SyscallError { + PermissionDenied, + InvalidOp, + SlotOccupied, + AlreadyMapped, + NotMapped, + InvalidPointer, + Unknown, +} + +impl SyscallError { + pub fn from(val: u64) -> SyscallError { + match val { + 1 => SyscallError::PermissionDenied, + 2 => SyscallError::InvalidOp, + 3 => SyscallError::SlotOccupied, + 4 => SyscallError::AlreadyMapped, + 5 => SyscallError::NotMapped, + 6 => SyscallError::InvalidPointer, + _ => SyscallError::Unknown, + } + } +} + /// Single syscall ABI /// /// Entry: SVC #0 @@ -44,7 +74,7 @@ /// x1 = return value 0 /// x2 = return value 1 (if needed) #[inline(always)] -pub unsafe fn protected_call6( +unsafe fn syscall6( cap: u32, op: u32, a0: u64, @@ -57,59 +87,98 @@ pub unsafe fn protected_call6( let r0: u64; let r1: u64; let r2: u64; - - core::arch::asm!( - "svc #0", - inlateout("x0") cap as u64 => r0, - inlateout("x1") op as u64 => r1, - inlateout("x2") a0 => r2, - in("x3") a1, - in("x4") a2, - in("x5") a3, - in("x6") a4, - in("x7") a5, - options(nostack), - ); - + unsafe { + core::arch::asm!( + "svc #0", + inlateout("x0") cap as u64 => r0, + inlateout("x1") op as u64 => r1, + inlateout("x2") a0 => r2, + in("x3") a1, + in("x4") a2, + in("x5") a3, + in("x6") a4, + in("x7") a5, + options(nostack), + ); + } (r0, r1, r2) } +#[inline(always)] +pub unsafe fn protected_call6( + cap: u32, + op: u32, + a0: u64, + a1: u64, + a2: u64, + a3: u64, + a4: u64, + a5: u64, +) -> SyscallResult { + let (ret, val0, val1) = unsafe { syscall6(cap, op, a0, a1, a2, a3, a4, a5) }; + if ret == 0 { + return Ok((val0, val1)); + } else { + return Err(SyscallError::from(ret)); + } +} + // Most operations don't need all 6 args - provide convenience wrappers // TODO: don't waste registers to zero them out for a less-than-6-args versions? Might be risky... /// 0-arg invoke (cap + op only) #[inline(always)] -pub fn protected_call0(cap: u32, op: u32) -> Result { - let (err, val, _) = unsafe { protected_call6(cap, op, 0, 0, 0, 0, 0, 0) }; - if err == 0 { Ok(val) } else { Err(Error(err)) } +pub fn protected_call0(cap: u32, op: u32) -> SyscallResult { + let (ret, val0, val1) = unsafe { syscall6(cap, op, 0, 0, 0, 0, 0, 0) }; + if ret == 0 { + return Ok((val0, val1)); + } else { + return Err(SyscallError::from(ret)); + } } /// 1-arg invoke #[inline(always)] -pub fn protected_call1(cap: u32, op: u32, a0: u64) -> Result { - let (err, val, _) = unsafe { protected_call6(cap, op, a0, 0, 0, 0, 0, 0) }; - if err == 0 { Ok(val) } else { Err(Error(err)) } +pub fn protected_call1(cap: u32, op: u32, a0: u64) -> SyscallResult { + let (ret, val0, val1) = unsafe { syscall6(cap, op, a0, 0, 0, 0, 0, 0) }; + if ret == 0 { + return Ok((val0, val1)); + } else { + return Err(SyscallError::from(ret)); + } } /// 2-arg invoke #[inline(always)] -pub fn protected_call2(cap: u32, op: u32, a0: u64, a1: u64) -> Result { - let (err, val, _) = unsafe { protected_call6(cap, op, a0, a1, 0, 0, 0, 0) }; - if err == 0 { Ok(val) } else { Err(Error(err)) } +pub fn protected_call2(cap: u32, op: u32, a0: u64, a1: u64) -> SyscallResult { + let (ret, val0, val1) = unsafe { syscall6(cap, op, a0, a1, 0, 0, 0, 0) }; + if ret == 0 { + return Ok((val0, val1)); + } else { + return Err(SyscallError::from(ret)); + } } /// 3-arg invoke #[inline(always)] -pub fn protected_call3(cap: u32, op: u32, a0: u64, a1: u64, a2: u64) -> Result { - let (err, val, _) = unsafe { protected_call6(cap, op, a0, a1, a2, 0, 0, 0) }; - if err == 0 { Ok(val) } else { Err(Error(err)) } +pub fn protected_call3(cap: u32, op: u32, a0: u64, a1: u64, a2: u64) -> SyscallResult { + let (ret, val0, val1) = unsafe { syscall6(cap, op, a0, a1, a2, 0, 0, 0) }; + if ret == 0 { + return Ok((val0, val1)); + } else { + return Err(SyscallError::from(ret)); + } } /// 4-arg invoke #[inline(always)] -pub fn protected_call4(cap: u32, op: u32, a0: u64, a1: u64, a2: u64, a3: u64) -> Result { - let (err, val, _) = unsafe { protected_call6(cap, op, a0, a1, a2, a3, 0, 0) }; - if err == 0 { Ok(val) } else { Err(Error(err)) } +pub fn protected_call4(cap: u32, op: u32, a0: u64, a1: u64, a2: u64, a3: u64) -> SyscallResult { + let (ret, val0, val1) = unsafe { syscall6(cap, op, a0, a1, a2, a3, 0, 0) }; + if ret == 0 { + return Ok((val0, val1)); + } else { + return Err(SyscallError::from(ret)); + } } /// 5-arg invoke @@ -122,7 +191,11 @@ pub fn protected_call5( a2: u64, a3: u64, a4: u64, -) -> Result { - let (err, val, _) = unsafe { protected_call6(cap, op, a0, a1, a2, a3, a4, 0) }; - if err == 0 { Ok(val) } else { Err(Error(err)) } +) -> SyscallResult { + let (ret, val0, val1) = unsafe { syscall6(cap, op, a0, a1, a2, a3, a4, 0) }; + if ret == 0 { + return Ok((val0, val1)); + } else { + return Err(SyscallError::from(ret)); + } } From bb59857d61a47001542546c86f3933fc3d2e136f Mon Sep 17 00:00:00 2001 From: Berkus Decker Date: Thu, 29 Jan 2026 22:55:47 +0200 Subject: [PATCH 051/107] =?UTF-8?q?wip:=20=F0=9F=9A=A7=20arch-specific=20k?= =?UTF-8?q?ey=20types?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- kernel/nucleus/design.md | 9 + kernel/nucleus/src/api/aarch64_objects.rs | 371 ++++++++++++++++++++++ kernel/nucleus/src/api/arch_invoke.rs | 149 +++++++++ kernel/nucleus/src/api/arch_objects.rs | 67 ++++ kernel/nucleus/src/api/arch_pools.rs | 34 ++ kernel/nucleus/src/api/kernel_object.rs | 371 ++++++++++++++++++++++ kernel/nucleus/src/api/notification.rs | 46 +++ kernel/nucleus/src/api/object_pool.rs | 120 +++++++ kernel/nucleus/src/api/untyped.rs | 190 +++++++++++ kernel/nucleus/src/main.rs | 95 ++++++ kernel/nucleus/src/nucleus.rs | 59 ++++ 11 files changed, 1511 insertions(+) create mode 100644 kernel/nucleus/design.md create mode 100644 kernel/nucleus/src/api/aarch64_objects.rs create mode 100644 kernel/nucleus/src/api/arch_invoke.rs create mode 100644 kernel/nucleus/src/api/arch_objects.rs create mode 100644 kernel/nucleus/src/api/arch_pools.rs create mode 100644 kernel/nucleus/src/api/kernel_object.rs create mode 100644 kernel/nucleus/src/api/object_pool.rs create mode 100644 kernel/nucleus/src/nucleus.rs diff --git a/kernel/nucleus/design.md b/kernel/nucleus/design.md new file mode 100644 index 000000000..2b9263dd0 --- /dev/null +++ b/kernel/nucleus/design.md @@ -0,0 +1,9 @@ +Key Design Points + +Aspect | Design Choice | Rationale +Type numbering | Core 0-15, Arch 16-63 | Clear separation, room for growth +Arch trait | Associated types | Compile-time type safety per arch +Pools | Separate core + arch | Different lifecycles, easier to reason about +Frame sizes | Enum, not raw bits | Type-safe, arch-specific validation +VSpace | Wraps root PT + ASID | Clean abstraction for address space +Dispatch | Single match on ObjectType | Uniform handling, arch types get own handlers diff --git a/kernel/nucleus/src/api/aarch64_objects.rs b/kernel/nucleus/src/api/aarch64_objects.rs new file mode 100644 index 000000000..ed50d788c --- /dev/null +++ b/kernel/nucleus/src/api/aarch64_objects.rs @@ -0,0 +1,371 @@ +// ═══════════════════════════════════════════════════════════════════ +// AARCH64 ARCHITECTURE OBJECTS +// ═══════════════════════════════════════════════════════════════════ + +#[cfg(target_arch = "aarch64")] +pub mod aarch64 { + use super::*; + + /// AArch64-specific kernel objects + pub struct AArch64; + + impl ArchObjects for AArch64 { + type Frame = AArch64Frame; + type PageTable = AArch64PageTable; + type VSpace = AArch64VSpace; + type ASIDPool = AArch64ASIDPool; + type ASID = AArch64ASID; + + /// AArch64 supports 4KB, 2MB, and 1GB pages + const FRAME_SIZES: &'static [FrameSize] = &[ + FrameSize::Small, // 4KB + FrameSize::Large, // 2MB + FrameSize::Huge, // 1GB + ]; + + /// 4-level page tables (Sv48 equivalent) + const PT_LEVELS: usize = 4; + + /// 9 bits per level (512 entries) + const PT_INDEX_BITS: usize = 9; + + fn validate_retype(obj_type: ObjectType, size_bits: u8) -> Result { + match obj_type { + ObjectType::Frame => { + // Validate frame size + match size_bits { + 12 => Ok(4096), // 4KB + 21 => Ok(2 * 1024 * 1024), // 2MB + 30 => Ok(1024 * 1024 * 1024), // 1GB + _ => Err(CapError::InvalidFrameSize(size_bits)), + } + } + ObjectType::PageTable => { + // Page tables are always 4KB on AArch64 + if size_bits != 12 { + return Err(CapError::InvalidSize); + } + Ok(4096) + } + ObjectType::VSpace => { + // VSpace object is small metadata, but needs a root PT + Ok(core::mem::size_of::()) + } + ObjectType::ASIDPool => { + // Pool of 256 ASIDs + Ok(core::mem::size_of::()) + } + ObjectType::ASID => Ok(core::mem::size_of::()), + _ => Err(CapError::NotArchType), + } + } + + fn create_arch_object( + obj_type: ObjectType, + phys_addr: PhysAddr, + size_bits: u8, + pools: &mut ArchPools, + ) -> Result { + match obj_type { + ObjectType::Frame => { + let size = Self::validate_retype(obj_type, size_bits)?; + let frame = AArch64Frame::new(phys_addr, FrameSize::from_bits(size_bits)?); + let obj = pools + .frames + .allocate(frame) + .ok_or(CapError::PoolExhausted)?; + Ok(ObjectRef::new(obj)) + } + ObjectType::PageTable => { + let pt = AArch64PageTable::new(phys_addr); + let obj = pools + .page_tables + .allocate(pt) + .ok_or(CapError::PoolExhausted)?; + Ok(ObjectRef::new(obj)) + } + ObjectType::VSpace => { + let vspace = AArch64VSpace::new(); + let obj = pools + .vspaces + .allocate(vspace) + .ok_or(CapError::PoolExhausted)?; + Ok(ObjectRef::new(obj)) + } + ObjectType::ASIDPool => { + let pool = AArch64ASIDPool::new(); + let obj = pools + .asid_pools + .allocate(pool) + .ok_or(CapError::PoolExhausted)?; + Ok(ObjectRef::new(obj)) + } + ObjectType::ASID => { + // ASIDs are allocated from pools, not directly + Err(CapError::InvalidOperation) + } + _ => Err(CapError::NotArchType), + } + } + } + + // ───────────────────────────────────────────────────────────────── + // AArch64 Frame (Physical Page) + // ───────────────────────────────────────────────────────────────── + + /// A physical memory frame on AArch64. + /// + /// Frames can be 4KB, 2MB, or 1GB and can be mapped into VSpaces. + #[derive(Debug)] + pub struct AArch64Frame { + /// Physical address (aligned to frame size) + phys_addr: PhysAddr, + /// Frame size + size: FrameSize, + /// Is this device memory? (affects cacheability) + is_device: bool, + /// Mapping count (for shared frames) + map_count: u16, + } + + impl AArch64Frame { + pub fn new(phys_addr: PhysAddr, size: FrameSize) -> Self { + // Verify alignment + debug_assert!(phys_addr.as_u64() & ((1 << size.bits()) - 1) == 0); + + Self { + phys_addr, + size, + is_device: false, + map_count: 0, + } + } + + pub fn phys_addr(&self) -> PhysAddr { + self.phys_addr + } + + pub fn size(&self) -> FrameSize { + self.size + } + + pub fn is_mapped(&self) -> bool { + self.map_count > 0 + } + } + + impl KernelObject for AArch64Frame { + const TYPE: ObjectType = ObjectType::Frame; + } + + // ───────────────────────────────────────────────────────────────── + // AArch64 Page Table + // ───────────────────────────────────────────────────────────────── + + /// A page table on AArch64 (any level: L0/L1/L2/L3). + /// + /// Each table has 512 entries (9 bits of index). + /// The level is tracked for validation during mapping. + #[derive(Debug)] + pub struct AArch64PageTable { + /// Physical address of the table (4KB aligned) + phys_addr: PhysAddr, + /// Which level (0 = root, 3 = leaf for 4KB pages) + level: u8, + /// Number of valid entries + mapped_count: u16, + } + + impl AArch64PageTable { + /// Number of entries per table + pub const NUM_ENTRIES: usize = 512; + + pub fn new(phys_addr: PhysAddr) -> Self { + Self { + phys_addr, + level: 0, // Set when attached to VSpace + mapped_count: 0, + } + } + + /// Get the raw entries (for kernel manipulation) + /// + /// # Safety + /// Caller must ensure the physical memory is mapped. + pub unsafe fn entries(&self) -> &[PageTableEntry; Self::NUM_ENTRIES] { + let virt = phys_to_virt(self.phys_addr); + &*(virt.as_ptr() as *const [PageTableEntry; Self::NUM_ENTRIES]) + } + + pub unsafe fn entries_mut(&mut self) -> &mut [PageTableEntry; Self::NUM_ENTRIES] { + let virt = phys_to_virt(self.phys_addr); + &mut *(virt.as_mut_ptr() as *mut [PageTableEntry; Self::NUM_ENTRIES]) + } + } + + impl KernelObject for AArch64PageTable { + const TYPE: ObjectType = ObjectType::PageTable; + } + + /// AArch64 page table entry (64 bits) + #[repr(transparent)] + #[derive(Copy, Clone)] + pub struct PageTableEntry(u64); + + impl PageTableEntry { + pub const VALID: u64 = 1 << 0; + pub const TABLE: u64 = 1 << 1; // vs block + pub const AF: u64 = 1 << 10; // Access flag + pub const AP_RO: u64 = 1 << 7; // Read-only + pub const AP_EL0: u64 = 1 << 6; // User accessible + pub const UXN: u64 = 1 << 54; // User execute never + pub const PXN: u64 = 1 << 53; // Privileged execute never + + pub const fn empty() -> Self { + Self(0) + } + + pub const fn is_valid(&self) -> bool { + self.0 & Self::VALID != 0 + } + + pub fn set_table(&mut self, phys: PhysAddr) { + self.0 = phys.as_u64() | Self::VALID | Self::TABLE; + } + + pub fn set_block(&mut self, phys: PhysAddr, attrs: u64) { + self.0 = phys.as_u64() | Self::VALID | attrs; + } + + pub fn set_page(&mut self, phys: PhysAddr, attrs: u64) { + self.0 = phys.as_u64() | Self::VALID | Self::TABLE | attrs; + } + } + + // ───────────────────────────────────────────────────────────────── + // AArch64 VSpace (Virtual Address Space) + // ───────────────────────────────────────────────────────────────── + + /// A virtual address space on AArch64. + /// + /// Contains the root page table (L0) and associated ASID. + /// Each Domain has one VSpace for its user-space mappings (TTBR0). + /// The kernel VSpace (TTBR1) is shared. + #[derive(Debug)] + pub struct AArch64VSpace { + /// Root page table (L0) + root_pt: Option, + /// Assigned ASID (for TLB tagging) + asid: Option, + /// Is this active on any CPU? + active_cpus: u32, + } + + impl AArch64VSpace { + pub fn new() -> Self { + Self { + root_pt: None, + asid: None, + active_cpus: 0, + } + } + + /// Attach a root page table + pub fn set_root(&mut self, pt_slot: KeySlot) { + self.root_pt = Some(pt_slot); + } + + /// Assign an ASID + pub fn set_asid(&mut self, asid: u16) { + self.asid = Some(asid); + } + + /// Get TTBR0 value for this VSpace + pub fn ttbr0(&self, root_pt_phys: PhysAddr) -> u64 { + let asid = self.asid.unwrap_or(0) as u64; + (asid << 48) | root_pt_phys.as_u64() + } + } + + impl KernelObject for AArch64VSpace { + const TYPE: ObjectType = ObjectType::VSpace; + } + + // ───────────────────────────────────────────────────────────────── + // AArch64 ASID Management + // ───────────────────────────────────────────────────────────────── + + /// Pool of ASIDs for address space tagging. + /// + /// AArch64 supports 8-bit or 16-bit ASIDs (we assume 16-bit). + /// Each pool manages a range of 256 ASIDs. + #[derive(Debug)] + pub struct AArch64ASIDPool { + /// Base ASID for this pool + base: u16, + /// Bitmap of allocated ASIDs (256 bits = 4 u64s) + allocated: [u64; 4], + /// Number allocated + count: u16, + } + + impl AArch64ASIDPool { + pub fn new() -> Self { + Self { + base: 0, + allocated: [0; 4], + count: 0, + } + } + + /// Allocate an ASID from this pool + pub fn allocate(&mut self) -> Option { + for (i, word) in self.allocated.iter_mut().enumerate() { + if *word != !0 { + let bit = word.trailing_ones() as u16; + *word |= 1 << bit; + self.count += 1; + return Some(self.base + (i as u16 * 64) + bit); + } + } + None + } + + /// Release an ASID back to the pool + pub fn release(&mut self, asid: u16) -> bool { + let offset = asid - self.base; + if offset >= 256 { + return false; + } + let word = (offset / 64) as usize; + let bit = offset % 64; + if self.allocated[word] & (1 << bit) != 0 { + self.allocated[word] &= !(1 << bit); + self.count -= 1; + true + } else { + false + } + } + } + + impl KernelObject for AArch64ASIDPool { + const TYPE: ObjectType = ObjectType::ASIDPool; + } + + /// An allocated ASID (capability wrapper) + #[derive(Debug)] + pub struct AArch64ASID { + /// The actual ASID value + value: u16, + /// Pool it came from + pool_slot: KeySlot, + } + + impl KernelObject for AArch64ASID { + const TYPE: ObjectType = ObjectType::ASID; + } +} + +#[cfg(target_arch = "aarch64")] +pub use aarch64::*; diff --git a/kernel/nucleus/src/api/arch_invoke.rs b/kernel/nucleus/src/api/arch_invoke.rs new file mode 100644 index 000000000..cba2357f9 --- /dev/null +++ b/kernel/nucleus/src/api/arch_invoke.rs @@ -0,0 +1,149 @@ +// ═══════════════════════════════════════════════════════════════════ +// ARCHITECTURE-SPECIFIC API HANDLERS +// ═══════════════════════════════════════════════════════════════════ + +pub mod api { + pub mod arch { + use super::*; + + pub mod frame { + #[repr(u8)] + pub enum FrameOp { + /// Map frame into a VSpace at given virtual address + Map = 0, + /// Unmap frame from VSpace + Unmap = 1, + /// Get physical address (requires special rights) + GetAddress = 2, + /// Remap with different attributes + Remap = 3, + } + + pub fn invoke( + frame: &mut A::Frame, + rights: Rights, + op: u32, + args: &[u64; 6], + ) -> Result<(u64, u64), CapError> { + let op = FrameOp::try_from(op as u8).map_err(|_| CapError::InvalidOperation)?; + + match op { + FrameOp::Map => { + // args[0] = vspace_slot + // args[1] = virt_addr + // args[2] = attrs (R/W/X) + if !rights.contains(Rights::READ) { + return Err(CapError::InsufficientRights); + } + // Implementation depends on A::Frame + todo!("frame map") + } + FrameOp::Unmap => { + todo!("frame unmap") + } + FrameOp::GetAddress => { + if !rights.contains(Rights::GRANT) { + return Err(CapError::InsufficientRights); + } + // Return physical address + todo!("frame get_address") + } + FrameOp::Remap => { + todo!("frame remap") + } + } + } + } + + pub mod vspace { + #[repr(u8)] + pub enum VSpaceOp { + /// Assign root page table + SetRoot = 0, + /// Assign ASID + AssignASID = 1, + /// Activate (switch to this address space) + Activate = 2, + /// Get current ASID + GetASID = 3, + } + + pub fn invoke( + vspace: &mut A::VSpace, + rights: Rights, + op: u32, + args: &[u64; 6], + kernel: &mut Kernel, + ) -> Result<(u64, u64), CapError> { + let op = VSpaceOp::try_from(op as u8).map_err(|_| CapError::InvalidOperation)?; + + match op { + VSpaceOp::SetRoot => { + // args[0] = page_table_slot + todo!("vspace set_root") + } + VSpaceOp::AssignASID => { + // args[0] = asid_pool_slot + todo!("vspace assign_asid") + } + VSpaceOp::Activate => { + todo!("vspace activate") + } + VSpaceOp::GetASID => { + todo!("vspace get_asid") + } + } + } + } + + pub mod page_table { + #[repr(u8)] + pub enum PageTableOp { + /// Map a page table into parent table + Map = 0, + /// Unmap from parent + Unmap = 1, + } + + pub fn invoke( + pt: &mut A::PageTable, + rights: Rights, + op: u32, + args: &[u64; 6], + kernel: &mut Kernel, + ) -> Result<(u64, u64), CapError> { + todo!("page_table invoke") + } + } + + pub mod asid_pool { + #[repr(u8)] + pub enum ASIDPoolOp { + /// Allocate an ASID from this pool + Allocate = 0, + } + + pub fn invoke( + pool: &mut A::ASIDPool, + rights: Rights, + op: u32, + args: &[u64; 6], + kernel: &mut Kernel, + ) -> Result<(u64, u64), CapError> { + todo!("asid_pool invoke") + } + } + + pub mod asid { + pub fn invoke( + asid: &mut A::ASID, + rights: Rights, + op: u32, + args: &[u64; 6], + ) -> Result<(u64, u64), CapError> { + // ASIDs mostly just exist; operations are minimal + todo!("asid invoke") + } + } + } +} diff --git a/kernel/nucleus/src/api/arch_objects.rs b/kernel/nucleus/src/api/arch_objects.rs new file mode 100644 index 000000000..c21214d84 --- /dev/null +++ b/kernel/nucleus/src/api/arch_objects.rs @@ -0,0 +1,67 @@ +// ═══════════════════════════════════════════════════════════════════ +// ARCHITECTURE ABSTRACTION TRAIT +// ═══════════════════════════════════════════════════════════════════ + +/// Trait defining architecture-specific kernel object types and operations. +/// +/// Each architecture implements this trait to provide: +/// - Concrete types for frames, page tables, etc. +/// - Size/alignment requirements +/// - Retype validation +pub trait ArchObjects: Sized + 'static { + /// Physical memory frame type + type Frame: KernelObject; + /// Page table type (single level) + type PageTable: KernelObject; + /// Virtual address space root + type VSpace: KernelObject; + /// ASID pool type + type ASIDPool: KernelObject; + /// ASID type + type ASID: KernelObject; + + /// Supported frame sizes for this architecture + const FRAME_SIZES: &'static [FrameSize]; + + /// Number of page table levels + const PT_LEVELS: usize; + + /// Bits per page table level + const PT_INDEX_BITS: usize; + + /// Validate that an object type can be created with given size_bits + fn validate_retype(obj_type: ObjectType, size_bits: u8) -> Result; + + /// Create an architecture-specific object + fn create_arch_object( + obj_type: ObjectType, + phys_addr: PhysAddr, + size_bits: u8, + pools: &mut ArchPools, + ) -> Result; +} + +/// Frame size enumeration (common across architectures) +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub enum FrameSize { + /// 4KB (standard page) + Small, // 12 bits + /// 2MB (large page / section) + Large, // 21 bits + /// 1GB (huge page / supersection) + Huge, // 30 bits +} + +impl FrameSize { + pub const fn bits(&self) -> u8 { + match self { + FrameSize::Small => 12, + FrameSize::Large => 21, + FrameSize::Huge => 30, + } + } + + pub const fn size(&self) -> usize { + 1 << self.bits() + } +} diff --git a/kernel/nucleus/src/api/arch_pools.rs b/kernel/nucleus/src/api/arch_pools.rs new file mode 100644 index 000000000..3f7017fae --- /dev/null +++ b/kernel/nucleus/src/api/arch_pools.rs @@ -0,0 +1,34 @@ +// ═══════════════════════════════════════════════════════════════════ +// ARCHITECTURE-SPECIFIC OBJECT POOLS +// ═══════════════════════════════════════════════════════════════════ + +/// Pools for architecture-specific objects +pub struct ArchPools { + pub frames: ObjectPool, + pub page_tables: ObjectPool, + pub vspaces: ObjectPool, + pub asid_pools: ObjectPool, + pub asids: ObjectPool, +} + +impl ArchPools { + /// Create pools backed by untyped memory + /// + /// # Safety + /// Memory regions must be valid and non-overlapping + pub unsafe fn new( + frame_mem: (*mut u8, usize), + pt_mem: (*mut u8, usize), + vspace_mem: (*mut u8, usize), + asid_pool_mem: (*mut u8, usize), + asid_mem: (*mut u8, usize), + ) -> Self { + Self { + frames: ObjectPool::new(frame_mem.0, frame_mem.1), + page_tables: ObjectPool::new(pt_mem.0, pt_mem.1), + vspaces: ObjectPool::new(vspace_mem.0, vspace_mem.1), + asid_pools: ObjectPool::new(asid_pool_mem.0, asid_pool_mem.1), + asids: ObjectPool::new(asid_mem.0, asid_mem.1), + } + } +} diff --git a/kernel/nucleus/src/api/kernel_object.rs b/kernel/nucleus/src/api/kernel_object.rs new file mode 100644 index 000000000..355704731 --- /dev/null +++ b/kernel/nucleus/src/api/kernel_object.rs @@ -0,0 +1,371 @@ +//! Kernel object storage and capability lookup +//! +//! Design goals: +//! 1. Compact KeyEntry (fits in cache line) +//! 2. Type-safe access from handlers +//! 3. Objects live in typed pools (good for allocation) +//! 4. Support for derivation/revocation tree + +use core::ptr::NonNull; + +// ┌─────────────────────────────────────────────────────────────────────┐ +// │ ARCHITECTURE-SPECIFIC OBJECTS │ +// ├─────────────────────────────────────────────────────────────────────┤ +// │ │ +// │ Generic Kernel Objects Architecture-Specific Objects │ +// │ ───────────────────────── ───────────────────────────────── │ +// │ │ +// │ • Untyped AArch64: │ +// │ • Domain • Frame (4KB, 2MB, 1GB pages) │ +// │ • KeyTable • PageTable (translation table) │ +// │ • Notification • VSpace (TTBR0/TTBR1 root) │ +// │ • EventCount • ASIDPool (ASID allocation) │ +// │ • Endpoint • ASID (address space ID) │ +// │ • Time • IOSpace (SMMU for devices) │ +// │ • Buffer │ +// │ • Reply x86_64: │ +// │ • Frame (4KB, 2MB, 1GB pages) │ +// │ • PageTable (PML4/PDPT/PD/PT) │ +// │ • VSpace (CR3 root) │ +// │ • IOPort (x86 I/O ports) │ +// │ • IOSpace (VT-d for devices) │ +// │ │ +// │ RISC-V: │ +// │ • Frame (4KB, 2MB, 1GB) │ +// │ • PageTable (Sv39/Sv48) │ +// │ • VSpace (satp root) │ +// │ │ +// └─────────────────────────────────────────────────────────────────────┘ + +// ┌─────────────────────────────────────────────────────────────────────┐ +// │ OBJECT TYPE HIERARCHY │ +// ├─────────────────────────────────────────────────────────────────────┤ +// │ │ +// │ ObjectType (u8) │ +// │ ├── Core Types (0-15) │ +// │ │ ├── 0: Null │ +// │ │ ├── 1: Untyped ─→ Untyped struct │ +// │ │ ├── 2: Domain ─→ Domain struct │ +// │ │ ├── 3: KeyTable ─→ KeyTable struct │ +// │ │ ├── 4: Notification ─→ Notification struct │ +// │ │ ├── 5: EventCount ─→ EventCount struct │ +// │ │ ├── 6: Endpoint ─→ Endpoint struct │ +// │ │ ├── 7: Time ─→ TimeSlice struct │ +// │ │ ├── 8: Buffer ─→ Buffer struct │ +// │ │ └── 9: Reply ─→ Reply struct │ +// │ │ │ +// │ └── Arch Types (16-63) ──────────────────────────────────────────┐ │ +// │ │ │ │ +// │ │ ┌─────────────────────────────────────────────────────┐ │ │ +// │ │ │ impl ArchObjects for AArch64 │ │ │ +// │ │ │ type Frame = AArch64Frame │ │ │ +// │ │ │ type PageTable = AArch64PageTable │ │ │ +// │ │ │ type VSpace = AArch64VSpace │ │ │ +// │ │ │ type ASIDPool = AArch64ASIDPool │ │ │ +// │ │ │ type ASID = AArch64ASID │ │ │ +// │ │ └─────────────────────────────────────────────────────┘ │ │ +// │ │ │ │ +// │ ├── 16: Frame ─→ A::Frame │ │ +// │ ├── 17: PageTable ─→ A::PageTable │ │ +// │ ├── 18: VSpace ─→ A::VSpace │ │ +// │ ├── 19: ASIDPool ─→ A::ASIDPool │ │ +// │ ├── 20: ASID ─→ A::ASID │ │ +// │ ├── 21: IOSpace ─→ (SMMU/VT-d specific) │ │ +// │ ├── 22: IOPort ─→ (x86 only) │ │ +// │ ├── 23: IRQHandler ─→ IRQ binding │ │ +// │ └── 24: IRQControl ─→ IRQ management │ │ +// │ │ +// └─────────────────────────────────────────────────────────────────────┘ + +// ═══════════════════════════════════════════════════════════════════ +// OBJECT TYPE DISCRIMINANT - EXTENDED FOR ARCH TYPES +// ═══════════════════════════════════════════════════════════════════ + +/// Core object types (architecture-independent) +/// +/// These are the same across all architectures. +/// Values 0-63 are reserved for core types. +#[repr(u8)] +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub enum ObjectType { + // ─── Core Types (0-15) ─── + Null = 0, + Untyped = 1, + Domain = 2, + KeyTable = 3, + Notification = 4, + EventCount = 5, + Endpoint = 6, + Time = 7, + Buffer = 8, + Reply = 9, + + // Reserved for future core types: 10-15 + + // ─── Architecture-Specific Types (16-63) ─── + // These are defined per-architecture but share the enum space + // so we can have a single ObjectType for dispatch. + /// Physical memory frame (page) + Frame = 16, + /// Page table (any level) + PageTable = 17, + /// Virtual address space root + VSpace = 18, + /// ASID pool (AArch64, RISC-V) + ASIDPool = 19, + /// ASID control (AArch64, RISC-V) + ASID = 20, + /// I/O memory space (SMMU/VT-d) + IOSpace = 21, + /// I/O port range (x86 only) + IOPort = 22, + /// IRQ handler object + IRQHandler = 23, + /// IRQ control (for binding IRQs) + IRQControl = 24, + // Reserved for future arch types: 25-63 +} + +impl ObjectType { + /// Is this a core (architecture-independent) type? + #[inline] + pub const fn is_core(&self) -> bool { + (*self as u8) < 16 + } + + /// Is this an architecture-specific type? + #[inline] + pub const fn is_arch(&self) -> bool { + (*self as u8) >= 16 && (*self as u8) < 64 + } +} + +// ═══════════════════════════════════════════════════════════════════ +// KERNEL OBJECT TRAIT +// ═══════════════════════════════════════════════════════════════════ + +/// Marker trait for kernel objects - provides type → ObjectType mapping +pub trait KernelObject: Sized + 'static { + const TYPE: ObjectType; +} + +// Implement for each kernel object type +impl KernelObject for Untyped { + const TYPE: ObjectType = ObjectType::Untyped; +} +impl KernelObject for Domain { + const TYPE: ObjectType = ObjectType::Domain; +} +impl KernelObject for KeyTable { + const TYPE: ObjectType = ObjectType::KeyTable; +} +impl KernelObject for Notification { + const TYPE: ObjectType = ObjectType::Notification; +} +impl KernelObject for EventCount { + const TYPE: ObjectType = ObjectType::EventCount; +} +impl KernelObject for Endpoint { + const TYPE: ObjectType = ObjectType::Endpoint; +} +impl KernelObject for TimeSlice { + const TYPE: ObjectType = ObjectType::Time; +} +impl KernelObject for Buffer { + const TYPE: ObjectType = ObjectType::Buffer; +} +impl KernelObject for Reply { + const TYPE: ObjectType = ObjectType::Reply; +} + +// ═══════════════════════════════════════════════════════════════════ +// TYPE-ERASED OBJECT POINTER +// ═══════════════════════════════════════════════════════════════════ + +/// A type-erased pointer to a kernel object, with its type tag. +/// +/// This is the "fat pointer" alternative - we store the type alongside +/// the pointer so we can safely cast it back. +#[derive(Clone, Copy)] +pub struct ObjectRef { + ptr: NonNull<()>, + obj_type: ObjectType, +} + +impl ObjectRef { + /// Create a new object reference from a typed pointer + pub fn new(obj: &T) -> Self { + Self { + ptr: NonNull::from(obj).cast(), + obj_type: T::TYPE, + } + } + + /// Create from a mutable pointer (for objects in pools) + /// + /// # Safety + /// Caller must ensure the pointer is valid and properly aligned + pub unsafe fn from_raw(ptr: *mut T) -> Self { + Self { + ptr: NonNull::new_unchecked(ptr.cast()), + obj_type: T::TYPE, + } + } + + /// Get the object type + #[inline] + pub fn object_type(&self) -> ObjectType { + self.obj_type + } + + /// Attempt to cast to a specific type (immutable) + #[inline] + pub fn try_as(&self) -> Option<&T> { + if self.obj_type == T::TYPE { + // SAFETY: We verified the type matches + Some(unsafe { self.ptr.cast::().as_ref() }) + } else { + None + } + } + + /// Attempt to cast to a specific type (mutable) + #[inline] + pub fn try_as_mut(&mut self) -> Option<&mut T> { + if self.obj_type == T::TYPE { + // SAFETY: We verified the type matches + Some(unsafe { self.ptr.cast::().as_mut() }) + } else { + None + } + } + + /// Cast with error on type mismatch + #[inline] + pub fn as_type(&self) -> Result<&T, CapError> { + self.try_as().ok_or(CapError::TypeMismatch { + expected: T::TYPE, + found: self.obj_type, + }) + } + + /// Cast with error on type mismatch (mutable) + #[inline] + pub fn as_type_mut(&mut self) -> Result<&mut T, CapError> { + self.try_as_mut().ok_or(CapError::TypeMismatch { + expected: T::TYPE, + found: self.obj_type, + }) + } +} + +// ═══════════════════════════════════════════════════════════════════ +// KEY ENTRY (CAPABILITY TABLE ENTRY) +// ═══════════════════════════════════════════════════════════════════ + +/// A single entry in a domain's capability table (KeyTable). +/// +/// Size: 32 bytes (fits nicely in cache) +/// +/// ```text +/// ┌────────────────────────────────────────┐ +/// │ object_ref: ObjectRef (16 bytes) │ +/// │ - ptr: NonNull<()> (8 bytes) │ +/// │ - obj_type: ObjectType (1 byte) │ +/// │ - padding (7 bytes) │ +/// ├────────────────────────────────────────┤ +/// │ rights: Rights (2 bytes) │ +/// │ parent_slot: u16 (2 bytes) │ +/// │ badge: u32 (4 bytes) │ +/// │ gen: u32 (4 bytes) │ +/// │ padding (4 bytes) │ +/// └────────────────────────────────────────┘ +/// ``` +#[repr(C)] +pub struct KeyEntry { + /// Reference to the kernel object + object_ref: ObjectRef, + /// Access rights for this capability + rights: Rights, + /// Slot of parent capability (for revocation tree) + /// 0xFFFF = no parent (root capability) + parent_slot: u16, + /// Badge value (for endpoint discrimination, buffer offset, etc.) + badge: u32, + /// Generation counter (detect stale capabilities) + generation: u32, + _pad: u32, +} + +impl KeyEntry { + /// Create a null/empty entry + pub const fn null() -> Self { + Self { + object_ref: ObjectRef { + ptr: NonNull::dangling(), + obj_type: ObjectType::Null, + }, + rights: Rights::empty(), + parent_slot: 0xFFFF, + badge: 0, + generation: 0, + _pad: 0, + } + } + + /// Create a new capability entry + pub fn new( + object: &T, + rights: Rights, + badge: u32, + parent: Option, + ) -> Self { + Self { + object_ref: ObjectRef::new(object), + rights, + parent_slot: parent.map(|s| s.0).unwrap_or(0xFFFF), + badge, + generation: 0, + _pad: 0, + } + } + + /// Check if this entry is valid (not null) + #[inline] + pub fn is_valid(&self) -> bool { + self.object_ref.obj_type != ObjectType::Null + } + + /// Get the object type + #[inline] + pub fn object_type(&self) -> ObjectType { + self.object_ref.obj_type + } + + /// Get access rights + #[inline] + pub fn rights(&self) -> Rights { + self.rights + } + + /// Get badge value + #[inline] + pub fn badge(&self) -> u32 { + self.badge + } + + /// Access the underlying object with type checking + #[inline] + pub fn as_object(&self) -> Result<&T, CapError> { + self.object_ref.as_type() + } + + /// Access the underlying object mutably with type checking + #[inline] + pub fn as_object_mut(&mut self) -> Result<&mut T, CapError> { + self.object_ref.as_type_mut() + } +} + +// Verify size at compile time +const _: () = assert!(core::mem::size_of::() == 32); diff --git a/kernel/nucleus/src/api/notification.rs b/kernel/nucleus/src/api/notification.rs index 8d1663079..82f8e8ae5 100644 --- a/kernel/nucleus/src/api/notification.rs +++ b/kernel/nucleus/src/api/notification.rs @@ -79,3 +79,49 @@ pub fn invoke(cap: &Cap, op: u32, arg0: u64) -> SyscallResult { _ => Err(SyscallError::InvalidOp), } } + +pub fn invoke( + notify: &mut Notification, + rights: Rights, + badge: u32, + op: u32, + args: &[u64; 6], + ) -> Result<(u64, u64), CapError> { + let op = NotifyOp::try_from(op as u8) + .map_err(|_| CapError::InvalidOperation)?; + + match op { + NotifyOp::Signal => { + // Check we have send rights + if !rights.contains(Rights::SEND) { + return Err(CapError::InsufficientRights); + } + + // Signal using badge (or args[0] if badge is 0) + let bits = if badge != 0 { badge as u64 } else { args[0] }; + notify.signal(bits); + Ok((0, 0)) + } + + NotifyOp::Wait => { + // Check we have receive rights + if !rights.contains(Rights::RECV) { + return Err(CapError::InsufficientRights); + } + + let bits = notify.wait(current_domain_mut()); + Ok((bits, 0)) + } + + NotifyOp::Poll => { + if !rights.contains(Rights::RECV) { + return Err(CapError::InsufficientRights); + } + + let bits = notify.poll(); + Ok((bits, 0)) + } + } + } + } +} diff --git a/kernel/nucleus/src/api/object_pool.rs b/kernel/nucleus/src/api/object_pool.rs new file mode 100644 index 000000000..4deff55c3 --- /dev/null +++ b/kernel/nucleus/src/api/object_pool.rs @@ -0,0 +1,120 @@ +// ═══════════════════════════════════════════════════════════════════ +// OBJECT POOLS +// ═══════════════════════════════════════════════════════════════════ + +/// A pool of kernel objects of type T, backed by untyped memory. +/// +/// Objects are allocated via Untyped.Retype and live until revoked. +pub struct ObjectPool { + /// Base address of the pool + base: *mut T, + /// Bitmap of allocated slots + allocated: [u64; 4], // 256 objects max per pool + /// Number of allocated objects + count: u16, + /// Total capacity + capacity: u16, +} + +impl ObjectPool { + /// Create a new pool backed by untyped memory + /// + /// # Safety + /// The untyped memory must be properly sized and aligned for T + pub unsafe fn new(memory: *mut u8, size: usize) -> Self { + let capacity = (size / core::mem::size_of::()) as u16; + assert!(capacity <= 256); + + Self { + base: memory.cast(), + allocated: [0; 4], + count: 0, + capacity, + } + } + + /// Allocate an object in the pool, returning a reference + pub fn allocate(&mut self, init: T) -> Option<&mut T> { + // Find free slot + let slot = self.find_free_slot()?; + + // Mark as allocated + let word = slot / 64; + let bit = slot % 64; + self.allocated[word] |= 1 << bit; + self.count += 1; + + // Initialize the object + unsafe { + let ptr = self.base.add(slot); + ptr.write(init); + Some(&mut *ptr) + } + } + + /// Get a reference to an allocated object by index + pub fn get(&self, index: usize) -> Option<&T> { + if index >= self.capacity as usize { + return None; + } + + let word = index / 64; + let bit = index % 64; + if self.allocated[word] & (1 << bit) == 0 { + return None; + } + + unsafe { Some(&*self.base.add(index)) } + } + + /// Get a mutable reference to an allocated object + pub fn get_mut(&mut self, index: usize) -> Option<&mut T> { + if index >= self.capacity as usize { + return None; + } + + let word = index / 64; + let bit = index % 64; + if self.allocated[word] & (1 << bit) == 0 { + return None; + } + + unsafe { Some(&mut *self.base.add(index)) } + } + + /// Deallocate an object + pub fn deallocate(&mut self, index: usize) -> bool { + if index >= self.capacity as usize { + return false; + } + + let word = index / 64; + let bit = index % 64; + if self.allocated[word] & (1 << bit) == 0 { + return false; + } + + self.allocated[word] &= !(1 << bit); + self.count -= 1; + + // Drop the object + unsafe { + core::ptr::drop_in_place(self.base.add(index)); + } + + true + } + + fn find_free_slot(&self) -> Option { + for (word_idx, &word) in self.allocated.iter().enumerate() { + if word != !0 { + let bit = word.trailing_ones() as usize; + let slot = word_idx * 64 + bit; + if slot < self.capacity as usize { + return Some(slot); + } + } + } + None + } +} diff --git a/kernel/nucleus/src/api/untyped.rs b/kernel/nucleus/src/api/untyped.rs index 1ac8ff396..b65ab6a05 100644 --- a/kernel/nucleus/src/api/untyped.rs +++ b/kernel/nucleus/src/api/untyped.rs @@ -97,3 +97,193 @@ mod untyped_tests { let buf = BufferCap::::from_slot(slot_d, 1 << 16); } } + +// ═══════════════════════════════════════════════════════════════════ +// EXTENDED RETYPE WITH ARCH OBJECTS +// ═══════════════════════════════════════════════════════════════════ + +impl Untyped { + /// Retype this untyped memory into a kernel object. + /// + /// Handles both core and architecture-specific object types. + pub fn retype( + &mut self, + obj_type: ObjectType, + size_bits: u8, + dest_slot: KeySlot, + dest_keytable: &mut KeyTable, + pools: &mut KernelPools, + ) -> Result<(), CapError> { + // Determine object size based on type + let obj_size = if obj_type.is_core() { + core_object_size(obj_type, size_bits)? + } else { + A::validate_retype(obj_type, size_bits)? + }; + + // Check we have enough memory + if self.watermark + obj_size > self.size { + return Err(CapError::InsufficientMemory); + } + + // Allocate from untyped + let obj_addr = self.phys_addr.offset(self.watermark); + self.watermark += obj_size; + + // Create the object based on type + let entry = if obj_type.is_core() { + self.create_core_object(obj_type, obj_addr, size_bits, pools)? + } else { + self.create_arch_object::(obj_type, obj_addr, size_bits, pools)? + }; + + // Insert capability into destination slot + dest_keytable.insert(dest_slot, entry)?; + + Ok(()) + } + + fn create_core_object( + &mut self, + obj_type: ObjectType, + phys_addr: PhysAddr, + size_bits: u8, + pools: &mut KernelPools, + ) -> Result { + match obj_type { + ObjectType::Notification => { + let notify = Notification::new(); + let obj = pools + .notifications + .allocate(notify) + .ok_or(CapError::PoolExhausted)?; + Ok(KeyEntry::new(obj, Rights::all(), 0, None)) + } + + ObjectType::EventCount => { + let ec = EventCount::new(); + let obj = pools + .event_counts + .allocate(ec) + .ok_or(CapError::PoolExhausted)?; + Ok(KeyEntry::new(obj, Rights::all(), 0, None)) + } + + ObjectType::Domain => { + let domain = Domain::new(phys_addr); + let obj = pools + .domains + .allocate(domain) + .ok_or(CapError::PoolExhausted)?; + Ok(KeyEntry::new(obj, Rights::all(), 0, None)) + } + + ObjectType::Time => { + let time = TimeSlice::new_default(); + let obj = pools + .time_slices + .allocate(time) + .ok_or(CapError::PoolExhausted)?; + Ok(KeyEntry::new(obj, Rights::all(), 0, None)) + } + + ObjectType::Endpoint => { + let ep = Endpoint::new(); + let obj = pools + .endpoints + .allocate(ep) + .ok_or(CapError::PoolExhausted)?; + Ok(KeyEntry::new(obj, Rights::all(), 0, None)) + } + + ObjectType::Buffer => { + let size = 1usize << size_bits; + let buffer = Buffer::new(phys_addr, size); + let obj = pools + .buffers + .allocate(buffer) + .ok_or(CapError::PoolExhausted)?; + Ok(KeyEntry::new(obj, Rights::READ | Rights::WRITE, 0, None)) + } + + ObjectType::KeyTable => { + let kt = KeyTable::new_empty(); + let obj = pools + .keytables + .allocate(kt) + .ok_or(CapError::PoolExhausted)?; + Ok(KeyEntry::new(obj, Rights::all(), 0, None)) + } + + ObjectType::Untyped => { + // Split: create a child untyped + let child_size = 1usize << size_bits; + let child = Untyped { + phys_addr, + size: child_size, + watermark: 0, + is_device: self.is_device, + }; + let obj = pools + .untypeds + .allocate(child) + .ok_or(CapError::PoolExhausted)?; + Ok(KeyEntry::new(obj, Rights::all(), 0, None)) + } + + ObjectType::Reply => { + let reply = Reply::new(); + let obj = pools + .replies + .allocate(reply) + .ok_or(CapError::PoolExhausted)?; + // Reply caps have restricted rights + Ok(KeyEntry::new(obj, Rights::WRITE, 0, None)) + } + + _ => Err(CapError::InvalidObjectType), + } + } + + fn create_arch_object( + &mut self, + obj_type: ObjectType, + phys_addr: PhysAddr, + size_bits: u8, + pools: &mut KernelPools, + ) -> Result { + let obj_ref = A::create_arch_object(obj_type, phys_addr, size_bits, &mut pools.arch)?; + + // Default rights based on type + let rights = match obj_type { + ObjectType::Frame => Rights::READ | Rights::WRITE, + ObjectType::PageTable => Rights::all(), + ObjectType::VSpace => Rights::all(), + ObjectType::ASIDPool => Rights::all(), + ObjectType::ASID => Rights::all(), + _ => Rights::all(), + }; + + Ok(KeyEntry::from_ref(obj_ref, rights, 0, None)) + } +} + +fn core_object_size(obj_type: ObjectType, size_bits: u8) -> Result { + match obj_type { + ObjectType::Notification => Ok(core::mem::size_of::()), + ObjectType::EventCount => Ok(core::mem::size_of::()), + ObjectType::Time => Ok(core::mem::size_of::()), + ObjectType::Endpoint => Ok(core::mem::size_of::()), + ObjectType::Reply => Ok(core::mem::size_of::()), + ObjectType::Domain => Ok(4096), + ObjectType::KeyTable => Ok(core::mem::size_of::()), + ObjectType::Buffer | ObjectType::Untyped => { + if size_bits > 30 { + Err(CapError::InvalidSize) + } else { + Ok(1usize << size_bits) + } + } + _ => Err(CapError::InvalidObjectType), + } +} diff --git a/kernel/nucleus/src/main.rs b/kernel/nucleus/src/main.rs index 96c6f971d..67f8cfcc0 100644 --- a/kernel/nucleus/src/main.rs +++ b/kernel/nucleus/src/main.rs @@ -182,6 +182,101 @@ fn cap_invoke_handler( // } } +// ═══════════════════════════════════════════════════════════════════ +// SYSCALL DISPATCH WITH ARCH OBJECTS +// ═══════════════════════════════════════════════════════════════════ + +/// Main capability invocation handler +pub fn handle_cap_invoke( + kernel: &mut Kernel, + cap_slot: u32, + op: u32, + args: &[u64; 6], +) -> Result<(u64, u64), CapError> { + let domain = kernel.current_domain_mut()?; + let slot = KeySlot(cap_slot as u16); + let entry = domain.keytable.lookup_mut(slot)?; + + // Dispatch based on object type + match entry.object_type() { + // ─── Core Types ─── + ObjectType::Untyped => { + let untyped = entry.as_object_mut::()?; + api::untyped::invoke(untyped, entry.rights(), op, args, &mut kernel.pools) + } + + ObjectType::Notification => { + let notify = entry.as_object_mut::()?; + api::notification::invoke(notify, entry.rights(), entry.badge(), op, args) + } + + ObjectType::EventCount => { + let ec = entry.as_object_mut::()?; + api::event_count::invoke(ec, entry.rights(), op, args) + } + + ObjectType::Time => { + let time = entry.as_object_mut::()?; + api::time::invoke(time, entry.rights(), op, args, kernel) + } + + ObjectType::Domain => { + let target = entry.as_object_mut::()?; + api::domain::invoke(target, entry.rights(), op, args) + } + + ObjectType::Endpoint => { + let ep = entry.as_object_mut::()?; + api::endpoint::invoke(ep, entry.rights(), entry.badge(), op, args, kernel) + } + + ObjectType::Buffer => { + let buf = entry.as_object_mut::()?; + api::buffer::invoke(buf, entry.rights(), op, args) + } + + ObjectType::KeyTable => { + let kt = entry.as_object_mut::()?; + api::keytable::invoke(kt, entry.rights(), op, args) + } + + ObjectType::Reply => { + let reply = entry.as_object_mut::()?; + api::reply::invoke(reply, op, args, kernel) + } + + // ─── Architecture-Specific Types ─── + ObjectType::Frame => { + let frame = entry.as_object_mut::()?; + api::arch::frame::invoke::(frame, entry.rights(), op, args) + } + + ObjectType::PageTable => { + let pt = entry.as_object_mut::()?; + api::arch::page_table::invoke::(pt, entry.rights(), op, args, kernel) + } + + ObjectType::VSpace => { + let vspace = entry.as_object_mut::()?; + api::arch::vspace::invoke::(vspace, entry.rights(), op, args, kernel) + } + + ObjectType::ASIDPool => { + let pool = entry.as_object_mut::()?; + api::arch::asid_pool::invoke::(pool, entry.rights(), op, args, kernel) + } + + ObjectType::ASID => { + let asid = entry.as_object_mut::()?; + api::arch::asid::invoke::(asid, entry.rights(), op, args) + } + + ObjectType::Null => Err(CapError::NullCapability), + + _ => Err(CapError::UnknownObjectType(entry.object_type())), + } +} + fn get_pc() -> u64 { let pc: u64; unsafe { diff --git a/kernel/nucleus/src/nucleus.rs b/kernel/nucleus/src/nucleus.rs new file mode 100644 index 000000000..8d0a45b35 --- /dev/null +++ b/kernel/nucleus/src/nucleus.rs @@ -0,0 +1,59 @@ +// ┌─────────────────────────────────────────────────────────────────────┐ +// │ KERNEL TYPE STRUCTURE │ +// ├─────────────────────────────────────────────────────────────────────┤ +// │ │ +// │ Kernel │ +// │ │ │ +// │ ├── pools: KernelPools │ +// │ │ ├── untypeds: ObjectPool │ +// │ │ ├── domains: ObjectPool │ +// │ │ ├── keytables: ObjectPool │ +// │ │ ├── notifications: ObjectPool │ +// │ │ ├── event_counts: ObjectPool │ +// │ │ ├── endpoints: ObjectPool │ +// │ │ ├── time_slices: ObjectPool │ +// │ │ ├── buffers: ObjectPool │ +// │ │ ├── replies: ObjectPool │ +// │ │ │ │ +// │ │ └── arch: ArchPools │ +// │ │ ├── frames: ObjectPool │ +// │ │ ├── page_tables: ObjectPool │ +// │ │ ├── vspaces: ObjectPool │ +// │ │ ├── asid_pools: ObjectPool │ +// │ │ └── asids: ObjectPool │ +// │ │ │ +// │ ├── current_domain: Option │ +// │ └── dcb_pages: DcbPages │ +// │ │ +// └─────────────────────────────────────────────────────────────────────┘ + +// ═══════════════════════════════════════════════════════════════════ +// UNIFIED KERNEL OBJECT MANAGEMENT +// ═══════════════════════════════════════════════════════════════════ + +/// All kernel object pools - both core and architecture-specific +pub struct KernelPools { + // ─── Core Object Pools ─── + pub untypeds: ObjectPool, + pub domains: ObjectPool, + pub keytables: ObjectPool, + pub notifications: ObjectPool, + pub event_counts: ObjectPool, + pub endpoints: ObjectPool, + pub time_slices: ObjectPool, + pub buffers: ObjectPool, + pub replies: ObjectPool, + + // ─── Architecture-Specific Pools ─── + pub arch: ArchPools, +} + +/// Complete kernel state (parameterized by architecture) +pub struct Nucleus { + /// All object pools + pub pools: KernelPools, + /// Currently running domain + pub current_domain: Option, + /// DCB shared pages + pub dcb_pages: DcbPages, +} From f9016c8d6c2766f90b01e2d685ecb5e39ab52dc3 Mon Sep 17 00:00:00 2001 From: Berkus Decker Date: Thu, 29 Jan 2026 23:40:05 +0200 Subject: [PATCH 052/107] =?UTF-8?q?wip:=20=F0=9F=9A=A7=20two-phase=20arch-?= =?UTF-8?q?key=20dispatch?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- kernel/nucleus/design.md | 6 + kernel/nucleus/src/api/aarch64_objects.rs | 567 ++++++++++------------ kernel/nucleus/src/api/arch_objects.rs | 112 +++-- kernel/nucleus/src/api/kernel_object.rs | 3 + kernel/nucleus/src/main.rs | 317 +++++++++--- 5 files changed, 589 insertions(+), 416 deletions(-) diff --git a/kernel/nucleus/design.md b/kernel/nucleus/design.md index 2b9263dd0..1d91c22ff 100644 --- a/kernel/nucleus/design.md +++ b/kernel/nucleus/design.md @@ -7,3 +7,9 @@ Pools | Separate core + arch | Different lifecycles, easier to re Frame sizes | Enum, not raw bits | Type-safe, arch-specific validation VSpace | Wraps root PT + ASID | Clean abstraction for address space Dispatch | Single match on ObjectType | Uniform handling, arch types get own handlers + + +Kernel API surface: + +Wait options (open wait, like servers waiting; closed wait, like waiting one client's response specifically) +Timeouts for send and recv phases separately diff --git a/kernel/nucleus/src/api/aarch64_objects.rs b/kernel/nucleus/src/api/aarch64_objects.rs index ed50d788c..91d1fed00 100644 --- a/kernel/nucleus/src/api/aarch64_objects.rs +++ b/kernel/nucleus/src/api/aarch64_objects.rs @@ -1,371 +1,322 @@ // ═══════════════════════════════════════════════════════════════════ -// AARCH64 ARCHITECTURE OBJECTS +// AARCH64 IMPLEMENTATION // ═══════════════════════════════════════════════════════════════════ #[cfg(target_arch = "aarch64")] -pub mod aarch64 { - use super::*; - - /// AArch64-specific kernel objects - pub struct AArch64; - - impl ArchObjects for AArch64 { - type Frame = AArch64Frame; - type PageTable = AArch64PageTable; - type VSpace = AArch64VSpace; - type ASIDPool = AArch64ASIDPool; - type ASID = AArch64ASID; - - /// AArch64 supports 4KB, 2MB, and 1GB pages - const FRAME_SIZES: &'static [FrameSize] = &[ - FrameSize::Small, // 4KB - FrameSize::Large, // 2MB - FrameSize::Huge, // 1GB - ]; - - /// 4-level page tables (Sv48 equivalent) - const PT_LEVELS: usize = 4; - - /// 9 bits per level (512 entries) - const PT_INDEX_BITS: usize = 9; - - fn validate_retype(obj_type: ObjectType, size_bits: u8) -> Result { - match obj_type { - ObjectType::Frame => { - // Validate frame size - match size_bits { - 12 => Ok(4096), // 4KB - 21 => Ok(2 * 1024 * 1024), // 2MB - 30 => Ok(1024 * 1024 * 1024), // 1GB - _ => Err(CapError::InvalidFrameSize(size_bits)), - } - } - ObjectType::PageTable => { - // Page tables are always 4KB on AArch64 - if size_bits != 12 { - return Err(CapError::InvalidSize); - } +impl ArchObjects for AArch64 { + type Frame = AArch64Frame; + type PageTable = AArch64PageTable; + type VSpace = AArch64VSpace; + type ASIDPool = AArch64ASIDPool; + type ASID = AArch64ASID; + + const FRAME_SIZES: &'static [FrameSize] = + &[FrameSize::Small, FrameSize::Large, FrameSize::Huge]; + + const PT_LEVELS: usize = 4; + const PT_INDEX_BITS: usize = 9; + + fn validate_retype(arch_type: ArchType, size_bits: u8) -> Result { + match arch_type { + ArchType::Frame => match size_bits { + 12 => Ok(4096), + 21 => Ok(2 * 1024 * 1024), + 30 => Ok(1024 * 1024 * 1024), + _ => Err(CapError::InvalidFrameSize(size_bits)), + }, + ArchType::PageTable => { + if size_bits != 12 { + Err(CapError::InvalidSize) + } else { Ok(4096) } - ObjectType::VSpace => { - // VSpace object is small metadata, but needs a root PT - Ok(core::mem::size_of::()) - } - ObjectType::ASIDPool => { - // Pool of 256 ASIDs - Ok(core::mem::size_of::()) - } - ObjectType::ASID => Ok(core::mem::size_of::()), - _ => Err(CapError::NotArchType), } + ArchType::VSpace => Ok(core::mem::size_of::()), + ArchType::ASIDPool => Ok(core::mem::size_of::()), + ArchType::ASID => Ok(core::mem::size_of::()), + _ => Err(CapError::UnsupportedArchType(arch_type)), } + } - fn create_arch_object( - obj_type: ObjectType, - phys_addr: PhysAddr, - size_bits: u8, - pools: &mut ArchPools, - ) -> Result { - match obj_type { - ObjectType::Frame => { - let size = Self::validate_retype(obj_type, size_bits)?; - let frame = AArch64Frame::new(phys_addr, FrameSize::from_bits(size_bits)?); - let obj = pools - .frames - .allocate(frame) - .ok_or(CapError::PoolExhausted)?; - Ok(ObjectRef::new(obj)) - } - ObjectType::PageTable => { - let pt = AArch64PageTable::new(phys_addr); - let obj = pools - .page_tables - .allocate(pt) - .ok_or(CapError::PoolExhausted)?; - Ok(ObjectRef::new(obj)) - } - ObjectType::VSpace => { - let vspace = AArch64VSpace::new(); - let obj = pools - .vspaces - .allocate(vspace) - .ok_or(CapError::PoolExhausted)?; - Ok(ObjectRef::new(obj)) - } - ObjectType::ASIDPool => { - let pool = AArch64ASIDPool::new(); - let obj = pools - .asid_pools - .allocate(pool) - .ok_or(CapError::PoolExhausted)?; - Ok(ObjectRef::new(obj)) - } - ObjectType::ASID => { - // ASIDs are allocated from pools, not directly - Err(CapError::InvalidOperation) - } - _ => Err(CapError::NotArchType), + fn create_arch_object( + arch_type: ArchType, + phys_addr: PhysAddr, + size_bits: u8, + pools: &mut ArchPools, + ) -> Result { + match arch_type { + ArchType::Frame => { + let frame_size = FrameSize::from_bits(size_bits)?; + let frame = AArch64Frame::new(phys_addr, frame_size); + let obj = pools + .frames + .allocate(frame) + .ok_or(CapError::PoolExhausted)?; + Ok(ObjectRef::new(obj)) + } + ArchType::PageTable => { + let pt = AArch64PageTable::new(phys_addr); + let obj = pools + .page_tables + .allocate(pt) + .ok_or(CapError::PoolExhausted)?; + Ok(ObjectRef::new(obj)) + } + ArchType::VSpace => { + let vspace = AArch64VSpace::new(); + let obj = pools + .vspaces + .allocate(vspace) + .ok_or(CapError::PoolExhausted)?; + Ok(ObjectRef::new(obj)) } + ArchType::ASIDPool => { + let pool = AArch64ASIDPool::new(); + let obj = pools + .asid_pools + .allocate(pool) + .ok_or(CapError::PoolExhausted)?; + Ok(ObjectRef::new(obj)) + } + _ => Err(CapError::UnsupportedArchType(arch_type)), } } // ───────────────────────────────────────────────────────────────── - // AArch64 Frame (Physical Page) + // Frame Operations // ───────────────────────────────────────────────────────────────── - /// A physical memory frame on AArch64. - /// - /// Frames can be 4KB, 2MB, or 1GB and can be mapped into VSpaces. - #[derive(Debug)] - pub struct AArch64Frame { - /// Physical address (aligned to frame size) - phys_addr: PhysAddr, - /// Frame size - size: FrameSize, - /// Is this device memory? (affects cacheability) - is_device: bool, - /// Mapping count (for shared frames) - map_count: u16, - } - - impl AArch64Frame { - pub fn new(phys_addr: PhysAddr, size: FrameSize) -> Self { - // Verify alignment - debug_assert!(phys_addr.as_u64() & ((1 << size.bits()) - 1) == 0); - - Self { - phys_addr, - size, - is_device: false, - map_count: 0, - } - } - - pub fn phys_addr(&self) -> PhysAddr { - self.phys_addr + fn invoke_frame( + frame: &mut AArch64Frame, + rights: Rights, + op: u32, + args: &[u64; 6], + kernel: &mut Kernel, + ) -> Result<(u64, u64), CapError> { + #[repr(u8)] + enum FrameOp { + Map = 0, + Unmap = 1, + GetAddress = 2, + Remap = 3, } - pub fn size(&self) -> FrameSize { - self.size - } - - pub fn is_mapped(&self) -> bool { - self.map_count > 0 - } - } + let op = match op { + 0 => FrameOp::Map, + 1 => FrameOp::Unmap, + 2 => FrameOp::GetAddress, + 3 => FrameOp::Remap, + _ => return Err(CapError::InvalidOperation), + }; + + match op { + FrameOp::Map => { + // args[0] = vspace_slot + // args[1] = virt_addr + // args[2] = rights (R/W/X bits) + // args[3] = attrs (cacheability, etc.) + + if !rights.contains(Rights::READ) { + return Err(CapError::InsufficientRights); + } - impl KernelObject for AArch64Frame { - const TYPE: ObjectType = ObjectType::Frame; - } + let vspace_slot = KeySlot(args[0] as u16); + let virt_addr = VirtAddr::new(args[1]); + let map_rights = MapRights::from_bits(args[2] as u8); + let attrs = MemAttrs::from_bits(args[3] as u8); - // ───────────────────────────────────────────────────────────────── - // AArch64 Page Table - // ───────────────────────────────────────────────────────────────── + // Get the VSpace from the slot + let domain = kernel.current_domain()?; + let vspace_entry = domain.keytable.lookup(vspace_slot)?; + let vspace = vspace_entry.as_object::()?; - /// A page table on AArch64 (any level: L0/L1/L2/L3). - /// - /// Each table has 512 entries (9 bits of index). - /// The level is tracked for validation during mapping. - #[derive(Debug)] - pub struct AArch64PageTable { - /// Physical address of the table (4KB aligned) - phys_addr: PhysAddr, - /// Which level (0 = root, 3 = leaf for 4KB pages) - level: u8, - /// Number of valid entries - mapped_count: u16, - } + // Perform the mapping + aarch64_map_frame(frame, vspace, virt_addr, map_rights, attrs, kernel)?; - impl AArch64PageTable { - /// Number of entries per table - pub const NUM_ENTRIES: usize = 512; + Ok((0, 0)) + } - pub fn new(phys_addr: PhysAddr) -> Self { - Self { - phys_addr, - level: 0, // Set when attached to VSpace - mapped_count: 0, + FrameOp::Unmap => { + if frame.map_count == 0 { + return Err(CapError::NotMapped); + } + // ... unmap logic + Ok((0, 0)) } - } - /// Get the raw entries (for kernel manipulation) - /// - /// # Safety - /// Caller must ensure the physical memory is mapped. - pub unsafe fn entries(&self) -> &[PageTableEntry; Self::NUM_ENTRIES] { - let virt = phys_to_virt(self.phys_addr); - &*(virt.as_ptr() as *const [PageTableEntry; Self::NUM_ENTRIES]) - } + FrameOp::GetAddress => { + // Requires Grant right to expose physical address + if !rights.contains(Rights::GRANT) { + return Err(CapError::InsufficientRights); + } + Ok((frame.phys_addr.as_u64(), frame.size.size() as u64)) + } - pub unsafe fn entries_mut(&mut self) -> &mut [PageTableEntry; Self::NUM_ENTRIES] { - let virt = phys_to_virt(self.phys_addr); - &mut *(virt.as_mut_ptr() as *mut [PageTableEntry; Self::NUM_ENTRIES]) + FrameOp::Remap => { + // Change attributes on existing mapping + todo!("frame remap") + } } } - impl KernelObject for AArch64PageTable { - const TYPE: ObjectType = ObjectType::PageTable; - } - - /// AArch64 page table entry (64 bits) - #[repr(transparent)] - #[derive(Copy, Clone)] - pub struct PageTableEntry(u64); - - impl PageTableEntry { - pub const VALID: u64 = 1 << 0; - pub const TABLE: u64 = 1 << 1; // vs block - pub const AF: u64 = 1 << 10; // Access flag - pub const AP_RO: u64 = 1 << 7; // Read-only - pub const AP_EL0: u64 = 1 << 6; // User accessible - pub const UXN: u64 = 1 << 54; // User execute never - pub const PXN: u64 = 1 << 53; // Privileged execute never - - pub const fn empty() -> Self { - Self(0) - } - - pub const fn is_valid(&self) -> bool { - self.0 & Self::VALID != 0 - } + // ───────────────────────────────────────────────────────────────── + // Page Table Operations + // ───────────────────────────────────────────────────────────────── - pub fn set_table(&mut self, phys: PhysAddr) { - self.0 = phys.as_u64() | Self::VALID | Self::TABLE; + fn invoke_page_table( + pt: &mut AArch64PageTable, + rights: Rights, + op: u32, + args: &[u64; 6], + kernel: &mut Kernel, + ) -> Result<(u64, u64), CapError> { + #[repr(u8)] + enum PageTableOp { + Map = 0, // Map this PT into a parent PT or VSpace + Unmap = 1, // Unmap from parent } - pub fn set_block(&mut self, phys: PhysAddr, attrs: u64) { - self.0 = phys.as_u64() | Self::VALID | attrs; - } + let op = match op { + 0 => PageTableOp::Map, + 1 => PageTableOp::Unmap, + _ => return Err(CapError::InvalidOperation), + }; + + match op { + PageTableOp::Map => { + // args[0] = vspace_slot + // args[1] = virt_addr (determines which slot in parent) + let vspace_slot = KeySlot(args[0] as u16); + let virt_addr = VirtAddr::new(args[1]); + + // ... mapping logic + todo!("page_table map") + } - pub fn set_page(&mut self, phys: PhysAddr, attrs: u64) { - self.0 = phys.as_u64() | Self::VALID | Self::TABLE | attrs; + PageTableOp::Unmap => { + todo!("page_table unmap") + } } } // ───────────────────────────────────────────────────────────────── - // AArch64 VSpace (Virtual Address Space) + // VSpace Operations // ───────────────────────────────────────────────────────────────── - /// A virtual address space on AArch64. - /// - /// Contains the root page table (L0) and associated ASID. - /// Each Domain has one VSpace for its user-space mappings (TTBR0). - /// The kernel VSpace (TTBR1) is shared. - #[derive(Debug)] - pub struct AArch64VSpace { - /// Root page table (L0) - root_pt: Option, - /// Assigned ASID (for TLB tagging) - asid: Option, - /// Is this active on any CPU? - active_cpus: u32, - } - - impl AArch64VSpace { - pub fn new() -> Self { - Self { - root_pt: None, - asid: None, - active_cpus: 0, - } + fn invoke_vspace( + vspace: &mut AArch64VSpace, + rights: Rights, + op: u32, + args: &[u64; 6], + kernel: &mut Kernel, + ) -> Result<(u64, u64), CapError> { + #[repr(u8)] + enum VSpaceOp { + AssignASID = 0, + GetASID = 1, } - /// Attach a root page table - pub fn set_root(&mut self, pt_slot: KeySlot) { - self.root_pt = Some(pt_slot); - } + let op = match op { + 0 => VSpaceOp::AssignASID, + 1 => VSpaceOp::GetASID, + _ => return Err(CapError::InvalidOperation), + }; - /// Assign an ASID - pub fn set_asid(&mut self, asid: u16) { - self.asid = Some(asid); - } + match op { + VSpaceOp::AssignASID => { + // args[0] = asid_pool_slot + let pool_slot = KeySlot(args[0] as u16); - /// Get TTBR0 value for this VSpace - pub fn ttbr0(&self, root_pt_phys: PhysAddr) -> u64 { - let asid = self.asid.unwrap_or(0) as u64; - (asid << 48) | root_pt_phys.as_u64() - } - } + let domain = kernel.current_domain_mut()?; + let pool_entry = domain.keytable.lookup_mut(pool_slot)?; + let pool = pool_entry.as_object_mut::()?; - impl KernelObject for AArch64VSpace { - const TYPE: ObjectType = ObjectType::VSpace; + let asid = pool.allocate().ok_or(CapError::ASIDPoolExhausted)?; + + vspace.asid = Some(asid); + Ok((asid as u64, 0)) + } + + VSpaceOp::GetASID => { + let asid = vspace.asid.ok_or(CapError::NoASIDAssigned)?; + Ok((asid as u64, 0)) + } + } } // ───────────────────────────────────────────────────────────────── - // AArch64 ASID Management + // ASID Pool Operations // ───────────────────────────────────────────────────────────────── - /// Pool of ASIDs for address space tagging. - /// - /// AArch64 supports 8-bit or 16-bit ASIDs (we assume 16-bit). - /// Each pool manages a range of 256 ASIDs. - #[derive(Debug)] - pub struct AArch64ASIDPool { - /// Base ASID for this pool - base: u16, - /// Bitmap of allocated ASIDs (256 bits = 4 u64s) - allocated: [u64; 4], - /// Number allocated - count: u16, + fn invoke_asid_pool( + pool: &mut AArch64ASIDPool, + rights: Rights, + op: u32, + args: &[u64; 6], + _kernel: &mut Kernel, + ) -> Result<(u64, u64), CapError> { + // Most ASID operations go through VSpace.AssignASID + // Direct pool operations are rare + Err(CapError::InvalidOperation) } - impl AArch64ASIDPool { - pub fn new() -> Self { - Self { - base: 0, - allocated: [0; 4], - count: 0, - } - } + fn invoke_asid( + asid: &mut AArch64ASID, + rights: Rights, + op: u32, + args: &[u64; 6], + ) -> Result<(u64, u64), CapError> { + // ASID capabilities are mostly just tokens + // Operations would be for explicit invalidation + Err(CapError::InvalidOperation) + } +} - /// Allocate an ASID from this pool - pub fn allocate(&mut self) -> Option { - for (i, word) in self.allocated.iter_mut().enumerate() { - if *word != !0 { - let bit = word.trailing_ones() as u16; - *word |= 1 << bit; - self.count += 1; - return Some(self.base + (i as u16 * 64) + bit); - } - } - None - } +// ───────────────────────────────────────────────────────────────── +// AArch64 Frame (Physical Page) +// ───────────────────────────────────────────────────────────────── + +/// A physical memory frame on AArch64. +/// +/// Frames can be 4KB, 2MB, or 1GB and can be mapped into VSpaces. +#[derive(Debug)] +pub struct AArch64Frame { + /// Physical address (aligned to frame size) + phys_addr: PhysAddr, + /// Frame size + size: FrameSize, + /// Is this device memory? (affects cacheability) + is_device: bool, + /// Mapping count (for shared frames) + map_count: u16, +} - /// Release an ASID back to the pool - pub fn release(&mut self, asid: u16) -> bool { - let offset = asid - self.base; - if offset >= 256 { - return false; - } - let word = (offset / 64) as usize; - let bit = offset % 64; - if self.allocated[word] & (1 << bit) != 0 { - self.allocated[word] &= !(1 << bit); - self.count -= 1; - true - } else { - false - } +impl AArch64Frame { + pub fn new(phys_addr: PhysAddr, size: FrameSize) -> Self { + // Verify alignment + debug_assert!(phys_addr.as_u64() & ((1 << size.bits()) - 1) == 0); + + Self { + phys_addr, + size, + is_device: false, + map_count: 0, } } - impl KernelObject for AArch64ASIDPool { - const TYPE: ObjectType = ObjectType::ASIDPool; + pub fn phys_addr(&self) -> PhysAddr { + self.phys_addr } - /// An allocated ASID (capability wrapper) - #[derive(Debug)] - pub struct AArch64ASID { - /// The actual ASID value - value: u16, - /// Pool it came from - pool_slot: KeySlot, + pub fn size(&self) -> FrameSize { + self.size } - impl KernelObject for AArch64ASID { - const TYPE: ObjectType = ObjectType::ASID; + pub fn is_mapped(&self) -> bool { + self.map_count > 0 } } -#[cfg(target_arch = "aarch64")] -pub use aarch64::*; +impl KernelObject for AArch64Frame { + const TYPE: ObjectType = ObjectType::Frame; +} diff --git a/kernel/nucleus/src/api/arch_objects.rs b/kernel/nucleus/src/api/arch_objects.rs index c21214d84..79c9f726c 100644 --- a/kernel/nucleus/src/api/arch_objects.rs +++ b/kernel/nucleus/src/api/arch_objects.rs @@ -1,67 +1,97 @@ // ═══════════════════════════════════════════════════════════════════ -// ARCHITECTURE ABSTRACTION TRAIT +// ARCH OBJECTS TRAIT WITH INVOKE METHODS // ═══════════════════════════════════════════════════════════════════ -/// Trait defining architecture-specific kernel object types and operations. -/// -/// Each architecture implements this trait to provide: -/// - Concrete types for frames, page tables, etc. -/// - Size/alignment requirements -/// - Retype validation +/// Architecture abstraction trait - extended with invoke methods pub trait ArchObjects: Sized + 'static { - /// Physical memory frame type + // ─── Associated Types ─── type Frame: KernelObject; - /// Page table type (single level) type PageTable: KernelObject; - /// Virtual address space root type VSpace: KernelObject; - /// ASID pool type type ASIDPool: KernelObject; - /// ASID type type ASID: KernelObject; - /// Supported frame sizes for this architecture + // ─── Constants ─── const FRAME_SIZES: &'static [FrameSize]; - - /// Number of page table levels const PT_LEVELS: usize; - - /// Bits per page table level const PT_INDEX_BITS: usize; - /// Validate that an object type can be created with given size_bits - fn validate_retype(obj_type: ObjectType, size_bits: u8) -> Result; + // ─── Validation ─── + fn validate_retype(arch_type: ArchType, size_bits: u8) -> Result; - /// Create an architecture-specific object + // ─── Object Creation ─── fn create_arch_object( - obj_type: ObjectType, + arch_type: ArchType, phys_addr: PhysAddr, size_bits: u8, pools: &mut ArchPools, ) -> Result; -} -/// Frame size enumeration (common across architectures) -#[derive(Copy, Clone, Debug, PartialEq, Eq)] -pub enum FrameSize { - /// 4KB (standard page) - Small, // 12 bits - /// 2MB (large page / section) - Large, // 21 bits - /// 1GB (huge page / supersection) - Huge, // 30 bits -} + // ─── Invocation Handlers ─── + fn invoke_frame( + frame: &mut Self::Frame, + rights: Rights, + op: u32, + args: &[u64; 6], + kernel: &mut Kernel, + ) -> Result<(u64, u64), CapError>; + + fn invoke_page_table( + pt: &mut Self::PageTable, + rights: Rights, + op: u32, + args: &[u64; 6], + kernel: &mut Kernel, + ) -> Result<(u64, u64), CapError>; + + fn invoke_vspace( + vspace: &mut Self::VSpace, + rights: Rights, + op: u32, + args: &[u64; 6], + kernel: &mut Kernel, + ) -> Result<(u64, u64), CapError>; + + fn invoke_asid_pool( + pool: &mut Self::ASIDPool, + rights: Rights, + op: u32, + args: &[u64; 6], + kernel: &mut Kernel, + ) -> Result<(u64, u64), CapError>; + + fn invoke_asid( + asid: &mut Self::ASID, + rights: Rights, + op: u32, + args: &[u64; 6], + ) -> Result<(u64, u64), CapError>; + + // Optional - default implementations return UnsupportedArchType + fn invoke_io_space( + _entry: &mut KeyEntry, + _op: u32, + _args: &[u64; 6], + _kernel: &mut Kernel, + ) -> Result<(u64, u64), CapError> { + Err(CapError::UnsupportedArchType(ArchType::IOSpace)) + } -impl FrameSize { - pub const fn bits(&self) -> u8 { - match self { - FrameSize::Small => 12, - FrameSize::Large => 21, - FrameSize::Huge => 30, - } + fn invoke_irq_handler( + _entry: &mut KeyEntry, + _op: u32, + _args: &[u64; 6], + _kernel: &mut Kernel, + ) -> Result<(u64, u64), CapError> { + Err(CapError::UnsupportedArchType(ArchType::IRQHandler)) } - pub const fn size(&self) -> usize { - 1 << self.bits() + fn invoke_irq_control( + _entry: &mut KeyEntry, + _op: u32, + _args: &[u64; 6], + _kernel: &mut Kernel, + ) -> Result<(u64, u64), CapError> { + Err(CapError::UnsupportedArchType(ArchType::IRQControl)) } } diff --git a/kernel/nucleus/src/api/kernel_object.rs b/kernel/nucleus/src/api/kernel_object.rs index 355704731..fe7633932 100644 --- a/kernel/nucleus/src/api/kernel_object.rs +++ b/kernel/nucleus/src/api/kernel_object.rs @@ -147,6 +147,9 @@ impl ObjectType { /// Marker trait for kernel objects - provides type → ObjectType mapping pub trait KernelObject: Sized + 'static { const TYPE: ObjectType; + + //TODO: add invoke here? + // fn invoke(obj: &Self::TYPE, op: u32, args: &[u64]) -> SyscallResult; } // Implement for each kernel object type diff --git a/kernel/nucleus/src/main.rs b/kernel/nucleus/src/main.rs index 67f8cfcc0..0f9d6ef27 100644 --- a/kernel/nucleus/src/main.rs +++ b/kernel/nucleus/src/main.rs @@ -39,36 +39,89 @@ fn panicked(info: &PanicInfo) -> ! { libmachine::panic::handler(info) } -// Kernel API surface: -// -// Wait options (open wait, like servers waiting; closed wait, like waiting one client's response specifically) -// Timeouts for send and recv phases separately - -// ----- -// ----- -// ----- -// ----- -// ----- -// ----- -// ----- -// ----- -// ----- -// ----- -// ----- -// ----- -// ----- -// ----- -// ----- - -// use crate::{ -// buffer::{BufferInfo, BufferOp}, -// endpoint::EndpointOp, -// }; - -/// Capability types now include: -pub enum ObjectType { +// ═══════════════════════════════════════════════════════════════════ +// OBJECT TYPE WITH ARCH BIT +// ═══════════════════════════════════════════════════════════════════ + +/// Object type discriminant with architectural bit. +/// +/// Bit 7 (high bit) indicates architecture-specific type. +/// +/// Layout: +/// +/// Bit 7 Bits 6-0 +/// ───── ──────── +/// 0 Core type (0-127) +/// 1 Arch type (0-127) +/// +#[repr(transparent)] +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub struct ObjectType(u8); + +impl ObjectType { + /// Bit indicating architecture-specific capability + pub const ARCH_BIT: u8 = 0x80; + + // ─── Core Types (0x00 - 0x7F) ─── + pub const NULL: Self = Self(0); + pub const UNTYPED: Self = Self(1); + pub const DOMAIN: Self = Self(2); + pub const KEY_TABLE: Self = Self(3); + pub const NOTIFICATION: Self = Self(4); + pub const EVENT_COUNT: Self = Self(5); + pub const ENDPOINT: Self = Self(6); + pub const TIME: Self = Self(7); + pub const BUFFER: Self = Self(8); + pub const REPLY: Self = Self(9); + // Reserved: 10-127 + + // ─── Arch Types (0x80 - 0xFF) ─── + pub const FRAME: Self = Self(Self::ARCH_BIT | 0); + pub const PAGE_TABLE: Self = Self(Self::ARCH_BIT | 1); + pub const VSPACE: Self = Self(Self::ARCH_BIT | 2); + pub const ASID_POOL: Self = Self(Self::ARCH_BIT | 3); + pub const ASID: Self = Self(Self::ARCH_BIT | 4); + pub const IO_SPACE: Self = Self(Self::ARCH_BIT | 5); + pub const IO_PORT: Self = Self(Self::ARCH_BIT | 6); // x86 only + pub const IRQ_HANDLER: Self = Self(Self::ARCH_BIT | 7); + pub const IRQ_CONTROL: Self = Self(Self::ARCH_BIT | 8); + // Reserved: 0x89 - 0xFF + + /// Check if this is an architecture-specific type. + #[inline(always)] + pub const fn is_arch(&self) -> bool { + (self.0 & Self::ARCH_BIT) != 0 + } + + /// Check if this is a core type. + #[inline(always)] + pub const fn is_core(&self) -> bool { + (self.0 & Self::ARCH_BIT) == 0 + } + + /// Get the type index within its category (strips arch bit). + #[inline(always)] + pub const fn index(&self) -> u8 { + self.0 & !Self::ARCH_BIT + } + + /// Raw value + #[inline(always)] + pub const fn as_u8(&self) -> u8 { + self.0 + } +} + +// ═══════════════════════════════════════════════════════════════════ +// CORE TYPE ENUM (FOR MATCH) +// ═══════════════════════════════════════════════════════════════════ + +/// Core object types - used for match dispatch after arch check +#[repr(u8)] +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub enum CoreType { /// No capability - None = 0, + Null = 0, /// Creates objects (including new key tables) Untyped = 1, /// Protection domain @@ -85,6 +138,76 @@ pub enum ObjectType { EventCount = 7, /// Shareable buffer capability Buffer = 8, + Reply = 9, +} + +impl TryFrom for CoreType { + type Error = CapError; + + #[inline] + fn try_from(ot: ObjectType) -> Result { + if ot.is_arch() { + return Err(CapError::NotCoreType); + } + match ot.index() { + 0 => Ok(CoreType::Null), + 1 => Ok(CoreType::Untyped), + 2 => Ok(CoreType::Domain), + 3 => Ok(CoreType::KeyTable), + 4 => Ok(CoreType::Notification), + 5 => Ok(CoreType::EventCount), + 6 => Ok(CoreType::Endpoint), + 7 => Ok(CoreType::Time), + 8 => Ok(CoreType::Buffer), + 9 => Ok(CoreType::Reply), + _ => Err(CapError::UnknownCoreType(ot.index())), + } + } +} + +// ═══════════════════════════════════════════════════════════════════ +// ARCH TYPE ENUM (ARCHITECTURE-SPECIFIC) +// ═══════════════════════════════════════════════════════════════════ + +/// Architecture-specific object types +/// +/// This is defined per-architecture but the indices are the same. +/// The actual struct types differ per architecture. +#[repr(u8)] +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub enum ArchType { + Frame = 0, + PageTable = 1, + VSpace = 2, + ASIDPool = 3, + ASID = 4, + IOSpace = 5, + IOPort = 6, + IRQHandler = 7, + IRQControl = 8, +} + +impl TryFrom for ArchType { + type Error = CapError; + + #[inline] + fn try_from(ot: ObjectType) -> Result { + if !ot.is_arch() { + return Err(CapError::NotArchType); + } + match ot.index() { + 0 => Ok(ArchType::Frame), + 1 => Ok(ArchType::PageTable), + 2 => Ok(ArchType::VSpace), + 3 => Ok(ArchType::ASIDPool), + 4 => Ok(ArchType::ASID), + 5 => Ok(ArchType::IOSpace), + 6 => Ok(ArchType::IOPort), + 7 => Ok(ArchType::IRQHandler), + 8 => Ok(ArchType::IRQControl), + _ => Err(CapError::UnknownArchType(ot.index())), + } + } } // Syscall handler - exception vector for EL0 synchronous exceptions @@ -186,7 +309,15 @@ fn cap_invoke_handler( // SYSCALL DISPATCH WITH ARCH OBJECTS // ═══════════════════════════════════════════════════════════════════ -/// Main capability invocation handler +/// Main capability invocation handler with two-level dispatch. +/// +/// First: single bit test to separate arch vs core +/// Then: smaller match within each category +/// +/// This is more branch-predictor friendly because: +/// 1. The arch bit test is highly predictable (most calls are core) +/// 2. Each sub-match has fewer cases +#[inline(never)] // Keep as separate function for branch prediction pub fn handle_cap_invoke( kernel: &mut Kernel, cap_slot: u32, @@ -196,84 +327,136 @@ pub fn handle_cap_invoke( let domain = kernel.current_domain_mut()?; let slot = KeySlot(cap_slot as u16); let entry = domain.keytable.lookup_mut(slot)?; + let obj_type = entry.object_type(); + + if obj_type.is_arch() { + // Architecture-specific dispatch (less common path) + arch_invoke::(kernel, entry, obj_type, op, args) + } else { + // Core dispatch (common path) + core_invoke::(kernel, entry, obj_type, op, args) + } +} - // Dispatch based on object type - match entry.object_type() { - // ─── Core Types ─── - ObjectType::Untyped => { +/// Core object dispatch - ~10 cases +#[inline(always)] +fn core_invoke( + kernel: &mut Kernel, + entry: &mut KeyEntry, + obj_type: ObjectType, + op: u32, + args: &[u64; 6], +) -> Result<(u64, u64), CapError> { + let core_type = CoreType::try_from(obj_type)?; + + match core_type { + CoreType::Null => Err(CapError::NullCapability), + + CoreType::Untyped => { let untyped = entry.as_object_mut::()?; api::untyped::invoke(untyped, entry.rights(), op, args, &mut kernel.pools) } - ObjectType::Notification => { - let notify = entry.as_object_mut::()?; - api::notification::invoke(notify, entry.rights(), entry.badge(), op, args) + CoreType::Domain => { + let domain = entry.as_object_mut::()?; + api::domain::invoke(domain, entry.rights(), op, args) } - ObjectType::EventCount => { - let ec = entry.as_object_mut::()?; - api::event_count::invoke(ec, entry.rights(), op, args) + CoreType::KeyTable => { + let kt = entry.as_object_mut::()?; + api::keytable::invoke(kt, entry.rights(), op, args) } - ObjectType::Time => { - let time = entry.as_object_mut::()?; - api::time::invoke(time, entry.rights(), op, args, kernel) + CoreType::Notification => { + let notify = entry.as_object_mut::()?; + api::notification::invoke(notify, entry.rights(), entry.badge(), op, args) } - ObjectType::Domain => { - let target = entry.as_object_mut::()?; - api::domain::invoke(target, entry.rights(), op, args) + CoreType::EventCount => { + let ec = entry.as_object_mut::()?; + api::event_count::invoke(ec, entry.rights(), op, args) } - ObjectType::Endpoint => { + CoreType::Endpoint => { let ep = entry.as_object_mut::()?; api::endpoint::invoke(ep, entry.rights(), entry.badge(), op, args, kernel) } - ObjectType::Buffer => { - let buf = entry.as_object_mut::()?; - api::buffer::invoke(buf, entry.rights(), op, args) + CoreType::Time => { + let time = entry.as_object_mut::()?; + api::time::invoke(time, entry.rights(), op, args, kernel) } - ObjectType::KeyTable => { - let kt = entry.as_object_mut::()?; - api::keytable::invoke(kt, entry.rights(), op, args) + CoreType::Buffer => { + let buf = entry.as_object_mut::()?; + api::buffer::invoke(buf, entry.rights(), op, args) } - ObjectType::Reply => { + CoreType::Reply => { let reply = entry.as_object_mut::()?; api::reply::invoke(reply, op, args, kernel) } + } +} - // ─── Architecture-Specific Types ─── - ObjectType::Frame => { +/// Architecture-specific dispatch - defined per architecture +#[inline(always)] +fn arch_invoke( + kernel: &mut Kernel, + entry: &mut KeyEntry, + obj_type: ObjectType, + op: u32, + args: &[u64; 6], +) -> Result<(u64, u64), CapError> { + let arch_type = ArchType::try_from(obj_type)?; + + match arch_type { + ArchType::Frame => { let frame = entry.as_object_mut::()?; - api::arch::frame::invoke::(frame, entry.rights(), op, args) + A::invoke_frame(frame, entry.rights(), op, args, kernel) // or A::Frame::invoke()? } - ObjectType::PageTable => { + ArchType::PageTable => { let pt = entry.as_object_mut::()?; - api::arch::page_table::invoke::(pt, entry.rights(), op, args, kernel) + A::invoke_page_table(pt, entry.rights(), op, args, kernel) } - ObjectType::VSpace => { + ArchType::VSpace => { let vspace = entry.as_object_mut::()?; - api::arch::vspace::invoke::(vspace, entry.rights(), op, args, kernel) + A::invoke_vspace(vspace, entry.rights(), op, args, kernel) } - ObjectType::ASIDPool => { + ArchType::ASIDPool => { let pool = entry.as_object_mut::()?; - api::arch::asid_pool::invoke::(pool, entry.rights(), op, args, kernel) + A::invoke_asid_pool(pool, entry.rights(), op, args, kernel) } - ObjectType::ASID => { + ArchType::ASID => { let asid = entry.as_object_mut::()?; - api::arch::asid::invoke::(asid, entry.rights(), op, args) + A::invoke_asid(asid, entry.rights(), op, args) + } + + ArchType::IOSpace => { + // May not be supported on all architectures + A::invoke_io_space(entry, op, args, kernel) + } + + ArchType::IOPort => { + // x86 only + #[cfg(target_arch = "x86_64")] + { + let port = entry.as_object_mut::()?; + x86_64::invoke_io_port(port, entry.rights(), op, args) + } + #[cfg(not(target_arch = "x86_64"))] + { + Err(CapError::UnsupportedArchType(arch_type)) + } } - ObjectType::Null => Err(CapError::NullCapability), + ArchType::IRQHandler => A::invoke_irq_handler(entry, op, args, kernel), - _ => Err(CapError::UnknownObjectType(entry.object_type())), + ArchType::IRQControl => A::invoke_irq_control(entry, op, args, kernel), } } From cdc19bcbe53f9582375e0f450c352015481adb9c Mon Sep 17 00:00:00 2001 From: Berkus Decker Date: Fri, 30 Jan 2026 00:13:21 +0200 Subject: [PATCH 053/107] refactor: arch objects --- kernel/Plan.md | 5 + .../src/api/{ => arch}/aarch64_objects.rs | 0 .../nucleus/src/api/{ => arch}/arch_pools.rs | 0 kernel/nucleus/src/api/arch/asid.rs | 9 + kernel/nucleus/src/api/arch/asid_pool.rs | 15 + kernel/nucleus/src/api/arch/frame.rs | 46 +++ kernel/nucleus/src/api/arch/page_table.rs | 17 + kernel/nucleus/src/api/arch/vspace.rs | 38 +++ kernel/nucleus/src/api/arch_invoke.rs | 149 --------- kernel/nucleus/src/api/buffer.rs | 4 + kernel/nucleus/src/api/domain.rs | 4 + kernel/nucleus/src/api/endpoint.rs | 4 + kernel/nucleus/src/api/event_count.rs | 4 + kernel/nucleus/src/api/key_entry.rs | 105 ++++++- kernel/nucleus/src/api/mod.rs | 11 +- kernel/nucleus/src/api/notification.rs | 4 + kernel/nucleus/src/api/object_type.rs | 169 ++++++++++ kernel/nucleus/src/api/reply.rs | 4 + kernel/nucleus/src/api/time.rs | 4 + kernel/nucleus/src/api/untyped.rs | 4 + kernel/nucleus/src/{api => }/arch_objects.rs | 0 kernel/nucleus/src/main.rs | 295 ++++-------------- .../kernel_object.rs => nucleus_object.rs} | 217 +------------ kernel/nucleus/src/{api => }/object_pool.rs | 2 + 24 files changed, 510 insertions(+), 600 deletions(-) rename kernel/nucleus/src/api/{ => arch}/aarch64_objects.rs (100%) rename kernel/nucleus/src/api/{ => arch}/arch_pools.rs (100%) create mode 100644 kernel/nucleus/src/api/arch/asid.rs create mode 100644 kernel/nucleus/src/api/arch/asid_pool.rs create mode 100644 kernel/nucleus/src/api/arch/frame.rs create mode 100644 kernel/nucleus/src/api/arch/page_table.rs create mode 100644 kernel/nucleus/src/api/arch/vspace.rs delete mode 100644 kernel/nucleus/src/api/arch_invoke.rs create mode 100644 kernel/nucleus/src/api/object_type.rs rename kernel/nucleus/src/{api => }/arch_objects.rs (100%) rename kernel/nucleus/src/{api/kernel_object.rs => nucleus_object.rs} (59%) rename kernel/nucleus/src/{api => }/object_pool.rs (96%) diff --git a/kernel/Plan.md b/kernel/Plan.md index 4af11884d..0dcbc0437 100644 --- a/kernel/Plan.md +++ b/kernel/Plan.md @@ -14,6 +14,11 @@ Steps: - [x] Enter kernel init in EL2 - this will be needed to set up kernel mappings - [x] Print DTB - [x] Print max RAM from DTB + +- [ ] Allocate a single key slot in a global domain struct +- [ ] Fill it with capability to DebugConsole +- [ ] Invoke DebugConsole.Write via syscall + - [ ] Print kernel covered area - [ ] Print KERNEL_HIGH_BASE - [ ] Print kernel mappings size and attribs diff --git a/kernel/nucleus/src/api/aarch64_objects.rs b/kernel/nucleus/src/api/arch/aarch64_objects.rs similarity index 100% rename from kernel/nucleus/src/api/aarch64_objects.rs rename to kernel/nucleus/src/api/arch/aarch64_objects.rs diff --git a/kernel/nucleus/src/api/arch_pools.rs b/kernel/nucleus/src/api/arch/arch_pools.rs similarity index 100% rename from kernel/nucleus/src/api/arch_pools.rs rename to kernel/nucleus/src/api/arch/arch_pools.rs diff --git a/kernel/nucleus/src/api/arch/asid.rs b/kernel/nucleus/src/api/arch/asid.rs new file mode 100644 index 000000000..58d302773 --- /dev/null +++ b/kernel/nucleus/src/api/arch/asid.rs @@ -0,0 +1,9 @@ +pub fn invoke( + asid: &mut A::ASID, + rights: Rights, + op: u32, + args: &[u64; 6], +) -> Result<(u64, u64), CapError> { + // ASIDs mostly just exist; operations are minimal + todo!("asid invoke") +} diff --git a/kernel/nucleus/src/api/arch/asid_pool.rs b/kernel/nucleus/src/api/arch/asid_pool.rs new file mode 100644 index 000000000..29313083c --- /dev/null +++ b/kernel/nucleus/src/api/arch/asid_pool.rs @@ -0,0 +1,15 @@ +#[repr(u8)] +pub enum ASIDPoolOp { + /// Allocate an ASID from this pool + Allocate = 0, +} + +pub fn invoke( + pool: &mut A::ASIDPool, + rights: Rights, + op: u32, + args: &[u64; 6], + kernel: &mut Kernel, +) -> Result<(u64, u64), CapError> { + todo!("asid_pool invoke") +} diff --git a/kernel/nucleus/src/api/arch/frame.rs b/kernel/nucleus/src/api/arch/frame.rs new file mode 100644 index 000000000..3eee60077 --- /dev/null +++ b/kernel/nucleus/src/api/arch/frame.rs @@ -0,0 +1,46 @@ +#[repr(u8)] +pub enum FrameOp { + /// Map frame into a VSpace at given virtual address + Map = 0, + /// Unmap frame from VSpace + Unmap = 1, + /// Get physical address (requires special rights) + GetAddress = 2, + /// Remap with different attributes + Remap = 3, +} + +pub fn invoke( + frame: &mut A::Frame, + rights: Rights, + op: u32, + args: &[u64; 6], +) -> Result<(u64, u64), CapError> { + let op = FrameOp::try_from(op as u8).map_err(|_| CapError::InvalidOperation)?; + + match op { + FrameOp::Map => { + // args[0] = vspace_slot + // args[1] = virt_addr + // args[2] = attrs (R/W/X) + if !rights.contains(Rights::READ) { + return Err(CapError::InsufficientRights); + } + // Implementation depends on A::Frame + todo!("frame map") + } + FrameOp::Unmap => { + todo!("frame unmap") + } + FrameOp::GetAddress => { + if !rights.contains(Rights::GRANT) { + return Err(CapError::InsufficientRights); + } + // Return physical address + todo!("frame get_address") + } + FrameOp::Remap => { + todo!("frame remap") + } + } +} diff --git a/kernel/nucleus/src/api/arch/page_table.rs b/kernel/nucleus/src/api/arch/page_table.rs new file mode 100644 index 000000000..c68d43022 --- /dev/null +++ b/kernel/nucleus/src/api/arch/page_table.rs @@ -0,0 +1,17 @@ +#[repr(u8)] +pub enum PageTableOp { + /// Map a page table into parent table + Map = 0, + /// Unmap from parent + Unmap = 1, +} + +pub fn invoke( + pt: &mut A::PageTable, + rights: Rights, + op: u32, + args: &[u64; 6], + kernel: &mut Kernel, +) -> Result<(u64, u64), CapError> { + todo!("page_table invoke") +} diff --git a/kernel/nucleus/src/api/arch/vspace.rs b/kernel/nucleus/src/api/arch/vspace.rs new file mode 100644 index 000000000..37b46f5af --- /dev/null +++ b/kernel/nucleus/src/api/arch/vspace.rs @@ -0,0 +1,38 @@ +#[repr(u8)] +pub enum VSpaceOp { + /// Assign root page table + SetRoot = 0, + /// Assign ASID + AssignASID = 1, + /// Activate (switch to this address space) + Activate = 2, + /// Get current ASID + GetASID = 3, +} + +pub fn invoke( + vspace: &mut A::VSpace, + rights: Rights, + op: u32, + args: &[u64; 6], + kernel: &mut Kernel, +) -> Result<(u64, u64), CapError> { + let op = VSpaceOp::try_from(op as u8).map_err(|_| CapError::InvalidOperation)?; + + match op { + VSpaceOp::SetRoot => { + // args[0] = page_table_slot + todo!("vspace set_root") + } + VSpaceOp::AssignASID => { + // args[0] = asid_pool_slot + todo!("vspace assign_asid") + } + VSpaceOp::Activate => { + todo!("vspace activate") + } + VSpaceOp::GetASID => { + todo!("vspace get_asid") + } + } +} diff --git a/kernel/nucleus/src/api/arch_invoke.rs b/kernel/nucleus/src/api/arch_invoke.rs deleted file mode 100644 index cba2357f9..000000000 --- a/kernel/nucleus/src/api/arch_invoke.rs +++ /dev/null @@ -1,149 +0,0 @@ -// ═══════════════════════════════════════════════════════════════════ -// ARCHITECTURE-SPECIFIC API HANDLERS -// ═══════════════════════════════════════════════════════════════════ - -pub mod api { - pub mod arch { - use super::*; - - pub mod frame { - #[repr(u8)] - pub enum FrameOp { - /// Map frame into a VSpace at given virtual address - Map = 0, - /// Unmap frame from VSpace - Unmap = 1, - /// Get physical address (requires special rights) - GetAddress = 2, - /// Remap with different attributes - Remap = 3, - } - - pub fn invoke( - frame: &mut A::Frame, - rights: Rights, - op: u32, - args: &[u64; 6], - ) -> Result<(u64, u64), CapError> { - let op = FrameOp::try_from(op as u8).map_err(|_| CapError::InvalidOperation)?; - - match op { - FrameOp::Map => { - // args[0] = vspace_slot - // args[1] = virt_addr - // args[2] = attrs (R/W/X) - if !rights.contains(Rights::READ) { - return Err(CapError::InsufficientRights); - } - // Implementation depends on A::Frame - todo!("frame map") - } - FrameOp::Unmap => { - todo!("frame unmap") - } - FrameOp::GetAddress => { - if !rights.contains(Rights::GRANT) { - return Err(CapError::InsufficientRights); - } - // Return physical address - todo!("frame get_address") - } - FrameOp::Remap => { - todo!("frame remap") - } - } - } - } - - pub mod vspace { - #[repr(u8)] - pub enum VSpaceOp { - /// Assign root page table - SetRoot = 0, - /// Assign ASID - AssignASID = 1, - /// Activate (switch to this address space) - Activate = 2, - /// Get current ASID - GetASID = 3, - } - - pub fn invoke( - vspace: &mut A::VSpace, - rights: Rights, - op: u32, - args: &[u64; 6], - kernel: &mut Kernel, - ) -> Result<(u64, u64), CapError> { - let op = VSpaceOp::try_from(op as u8).map_err(|_| CapError::InvalidOperation)?; - - match op { - VSpaceOp::SetRoot => { - // args[0] = page_table_slot - todo!("vspace set_root") - } - VSpaceOp::AssignASID => { - // args[0] = asid_pool_slot - todo!("vspace assign_asid") - } - VSpaceOp::Activate => { - todo!("vspace activate") - } - VSpaceOp::GetASID => { - todo!("vspace get_asid") - } - } - } - } - - pub mod page_table { - #[repr(u8)] - pub enum PageTableOp { - /// Map a page table into parent table - Map = 0, - /// Unmap from parent - Unmap = 1, - } - - pub fn invoke( - pt: &mut A::PageTable, - rights: Rights, - op: u32, - args: &[u64; 6], - kernel: &mut Kernel, - ) -> Result<(u64, u64), CapError> { - todo!("page_table invoke") - } - } - - pub mod asid_pool { - #[repr(u8)] - pub enum ASIDPoolOp { - /// Allocate an ASID from this pool - Allocate = 0, - } - - pub fn invoke( - pool: &mut A::ASIDPool, - rights: Rights, - op: u32, - args: &[u64; 6], - kernel: &mut Kernel, - ) -> Result<(u64, u64), CapError> { - todo!("asid_pool invoke") - } - } - - pub mod asid { - pub fn invoke( - asid: &mut A::ASID, - rights: Rights, - op: u32, - args: &[u64; 6], - ) -> Result<(u64, u64), CapError> { - // ASIDs mostly just exist; operations are minimal - todo!("asid invoke") - } - } - } -} diff --git a/kernel/nucleus/src/api/buffer.rs b/kernel/nucleus/src/api/buffer.rs index 75c65a920..2f2d2e7ee 100644 --- a/kernel/nucleus/src/api/buffer.rs +++ b/kernel/nucleus/src/api/buffer.rs @@ -325,3 +325,7 @@ pub fn invoke(cap: &KeyEntry, op: u32, arg0: u64, arg1: u64) -> SyscallResult { } } } + +impl KernelObject for Buffer { + const TYPE: ObjectType = ObjectType::Buffer; +} diff --git a/kernel/nucleus/src/api/domain.rs b/kernel/nucleus/src/api/domain.rs index cdac1dc26..b2b48f51e 100644 --- a/kernel/nucleus/src/api/domain.rs +++ b/kernel/nucleus/src/api/domain.rs @@ -357,3 +357,7 @@ pub fn invoke(cap: &Cap, op: u32, arg0: u64, arg1: u64) -> SyscallResult { _ => Err(SyscallError::InvalidOp), } } + +impl KernelObject for Domain { + const TYPE: ObjectType = ObjectType::Domain; +} diff --git a/kernel/nucleus/src/api/endpoint.rs b/kernel/nucleus/src/api/endpoint.rs index 0e14c9e8c..192a79f7b 100644 --- a/kernel/nucleus/src/api/endpoint.rs +++ b/kernel/nucleus/src/api/endpoint.rs @@ -493,3 +493,7 @@ pub fn invoke(cap: &Cap, op: u32, arg0: u64, arg1: u64) -> SyscallResult { _ => Err(SyscallError::InvalidOp), } } + +impl KernelObject for Endpoint { + const TYPE: ObjectType = ObjectType::Endpoint; +} diff --git a/kernel/nucleus/src/api/event_count.rs b/kernel/nucleus/src/api/event_count.rs index f27017e31..0cb46c963 100644 --- a/kernel/nucleus/src/api/event_count.rs +++ b/kernel/nucleus/src/api/event_count.rs @@ -101,3 +101,7 @@ pub fn invoke(cap: &Cap, op: u32, arg0: u64) -> SyscallResult { _ => Err(SyscallError::InvalidOp), } } + +impl KernelObject for EventCount { + const TYPE: ObjectType = ObjectType::EventCount; +} diff --git a/kernel/nucleus/src/api/key_entry.rs b/kernel/nucleus/src/api/key_entry.rs index 1c0581f0c..125085a06 100644 --- a/kernel/nucleus/src/api/key_entry.rs +++ b/kernel/nucleus/src/api/key_entry.rs @@ -1,5 +1,106 @@ -// Trait-like wrapper around kernel objects +// ═══════════════════════════════════════════════════════════════════ +// KEY ENTRY (CAPABILITY TABLE ENTRY) +// ═══════════════════════════════════════════════════════════════════ + +/// A single entry in a domain's capability table (KeyTable). +/// +/// Size: 32 bytes (fits nicely in cache) +/// +/// ┌────────────────────────────────────────┐ +/// │ object_ref: ObjectRef (16 bytes) │ +/// │ - ptr: NonNull<()> (8 bytes) │ +/// │ - obj_type: ObjectType (1 byte) │ +/// │ - padding (7 bytes) │ +/// ├────────────────────────────────────────┤ +/// │ rights: Rights (2 bytes) │ +/// │ parent_slot: u16 (2 bytes) │ +/// │ badge: u32 (4 bytes) │ +/// │ gen: u32 (4 bytes) │ +/// │ padding (4 bytes) │ +/// └────────────────────────────────────────┘ +/// +#[repr(align(32))] +pub struct KeyEntry { + /// Reference to the kernel object + object_ref: ObjectRef, + /// Access rights for this capability + rights: Rights, + /// Slot of parent capability (for revocation tree) + /// 0xFFFF = no parent (root capability) + parent_slot: u16, + /// Badge value (for endpoint discrimination, buffer offset, etc.) + badge: u32, + /// Generation counter (detect stale capabilities) + generation: u32, +} impl KeyEntry { - pub fn as_debug_console() -> Result {} + /// Create a null/empty entry + pub const fn null() -> Self { + Self { + object_ref: ObjectRef { + ptr: NonNull::dangling(), + obj_type: ObjectType::Null, + }, + rights: Rights::empty(), + parent_slot: 0xFFFF, + badge: 0, + generation: 0, + } + } + + /// Create a new capability entry + pub fn new( + object: &T, + rights: Rights, + badge: u32, + parent: Option, + ) -> Self { + Self { + object_ref: ObjectRef::new(object), + rights, + parent_slot: parent.map(|s| s.0).unwrap_or(0xFFFF), + badge, + generation: 0, + } + } + + /// Check if this entry is valid (not null) + #[inline] + pub fn is_valid(&self) -> bool { + self.object_ref.obj_type != ObjectType::Null + } + + /// Get the object type + #[inline] + pub fn object_type(&self) -> ObjectType { + self.object_ref.obj_type + } + + /// Get access rights + #[inline] + pub fn rights(&self) -> Rights { + self.rights + } + + /// Get badge value + #[inline] + pub fn badge(&self) -> u32 { + self.badge + } + + /// Access the underlying object with type checking + #[inline] + pub fn as_object(&self) -> Result<&T, CapError> { + self.object_ref.as_type() + } + + /// Access the underlying object mutably with type checking + #[inline] + pub fn as_object_mut(&mut self) -> Result<&mut T, CapError> { + self.object_ref.as_type_mut() + } } + +// Verify size at compile time +const _: () = assert!(core::mem::size_of::() == 32); diff --git a/kernel/nucleus/src/api/mod.rs b/kernel/nucleus/src/api/mod.rs index 4afe42730..2eff4f7bd 100644 --- a/kernel/nucleus/src/api/mod.rs +++ b/kernel/nucleus/src/api/mod.rs @@ -1,14 +1,7 @@ // TODO: move these api decls/impls to libraries in libs/, re-export only user part through // TODO: libsyscall or something like that - usable from userspace. -// mod buffer; mod debug_console; -// mod domain; -// mod endpoint; -// mod event_count; mod key; -// mod key_table; -// mod notification; -// mod reply; -// mod time; -// mod untyped; +mod nucleus_object; +mod object_type; diff --git a/kernel/nucleus/src/api/notification.rs b/kernel/nucleus/src/api/notification.rs index 82f8e8ae5..16c472b07 100644 --- a/kernel/nucleus/src/api/notification.rs +++ b/kernel/nucleus/src/api/notification.rs @@ -125,3 +125,7 @@ pub fn invoke( } } } + +impl KernelObject for Notification { + const TYPE: ObjectType = ObjectType::Notification; +} diff --git a/kernel/nucleus/src/api/object_type.rs b/kernel/nucleus/src/api/object_type.rs new file mode 100644 index 000000000..9c4ae0e2b --- /dev/null +++ b/kernel/nucleus/src/api/object_type.rs @@ -0,0 +1,169 @@ +/// Object type discriminant with architectural bit. +/// +/// Bit 7 (high bit) indicates architecture-specific type. +/// +/// Layout: +/// +/// Bit 7 Bits 6-0 +/// ───── ──────── +/// 0 Core type (0-127) +/// 1 Arch type (0-127) +/// +#[repr(transparent)] +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub struct ObjectType(u8); + +impl ObjectType { + /// Bit indicating architecture-specific capability + pub const ARCH_BIT: u8 = 0x80; + + // ─── Core Types (0x00 - 0x7F) ─── + pub const NULL: Self = Self(0); + pub const UNTYPED: Self = Self(1); + pub const DOMAIN: Self = Self(2); + pub const KEY_TABLE: Self = Self(3); + pub const NOTIFICATION: Self = Self(4); + pub const EVENT_COUNT: Self = Self(5); + pub const ENDPOINT: Self = Self(6); + pub const TIME: Self = Self(7); + pub const BUFFER: Self = Self(8); + pub const REPLY: Self = Self(9); + // Reserved: 10-126 + pub const DEBUG_CONSOLE: Self = Self(127); // only #cfg(debug) + + // ─── Arch Types (0x80 - 0xFF) ─── + pub const FRAME: Self = Self(Self::ARCH_BIT | 0); + pub const PAGE_TABLE: Self = Self(Self::ARCH_BIT | 1); + pub const VSPACE: Self = Self(Self::ARCH_BIT | 2); + pub const ASID_POOL: Self = Self(Self::ARCH_BIT | 3); + pub const ASID: Self = Self(Self::ARCH_BIT | 4); + pub const IO_SPACE: Self = Self(Self::ARCH_BIT | 5); + pub const IO_PORT: Self = Self(Self::ARCH_BIT | 6); // x86 only + pub const IRQ_HANDLER: Self = Self(Self::ARCH_BIT | 7); + pub const IRQ_CONTROL: Self = Self(Self::ARCH_BIT | 8); + // Reserved: 0x89 - 0xFF + + /// Check if this is an architecture-specific type. + #[inline(always)] + pub const fn is_arch(&self) -> bool { + (self.0 & Self::ARCH_BIT) != 0 + } + + /// Check if this is a core type. + #[inline(always)] + pub const fn is_core(&self) -> bool { + (self.0 & Self::ARCH_BIT) == 0 + } + + /// Get the type index within its category (strips arch bit). + #[inline(always)] + pub const fn index(&self) -> u8 { + self.0 & !Self::ARCH_BIT + } + + /// Raw value + #[inline(always)] + pub const fn as_u8(&self) -> u8 { + self.0 + } +} + +// ═══════════════════════════════════════════════════════════════════ +// CORE TYPE ENUM (FOR MATCH) +// ═══════════════════════════════════════════════════════════════════ + +/// Core object types - used for match dispatch after arch check +#[repr(u8)] +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub enum CoreType { + /// No capability + Null = 0, + /// Creates objects (including new key tables) + Untyped = 1, + /// Protection domain + Domain = 2, + /// capability table itself + KeyTable = 3, + /// Time capability + Time = 4, + /// Endpoint capability + Endpoint = 5, + /// Notification endpoint capability + Notification = 6, + /// Event count endpoint capability + EventCount = 7, + /// Shareable buffer capability + Buffer = 8, + Reply = 9, + DebugConsole = 127, +} + +impl TryFrom for CoreType { + type Error = CapError; + + #[inline] + fn try_from(ot: ObjectType) -> Result { + if ot.is_arch() { + return Err(CapError::NotCoreType); + } + match ot.index() { + 0 => Ok(CoreType::Null), + 1 => Ok(CoreType::Untyped), + 2 => Ok(CoreType::Domain), + 3 => Ok(CoreType::KeyTable), + 4 => Ok(CoreType::Notification), + 5 => Ok(CoreType::EventCount), + 6 => Ok(CoreType::Endpoint), + 7 => Ok(CoreType::Time), + 8 => Ok(CoreType::Buffer), + 9 => Ok(CoreType::Reply), + 127 => Ok(CoreType::DebugConsole), + _ => Err(CapError::UnknownCoreType(ot.index())), + } + } +} + +// ═══════════════════════════════════════════════════════════════════ +// ARCH TYPE ENUM (ARCHITECTURE-SPECIFIC) +// ═══════════════════════════════════════════════════════════════════ + +/// Architecture-specific object types +/// +/// This is defined per-architecture but the indices are the same. +/// The actual struct types differ per architecture. +#[repr(u8)] +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub enum ArchType { + Frame = 0, + PageTable = 1, + VSpace = 2, + ASIDPool = 3, + ASID = 4, + IOSpace = 5, + IOPort = 6, + IRQHandler = 7, + IRQControl = 8, +} + +impl TryFrom for ArchType { + type Error = CapError; + + #[inline] + fn try_from(ot: ObjectType) -> Result { + if !ot.is_arch() { + return Err(CapError::NotArchType); + } + match ot.index() { + 0 => Ok(ArchType::Frame), + 1 => Ok(ArchType::PageTable), + 2 => Ok(ArchType::VSpace), + 3 => Ok(ArchType::ASIDPool), + 4 => Ok(ArchType::ASID), + 5 => Ok(ArchType::IOSpace), + 6 => Ok(ArchType::IOPort), + 7 => Ok(ArchType::IRQHandler), + 8 => Ok(ArchType::IRQControl), + _ => Err(CapError::UnknownArchType(ot.index())), + } + } +} diff --git a/kernel/nucleus/src/api/reply.rs b/kernel/nucleus/src/api/reply.rs index c1dc3ce70..ba26fadf2 100644 --- a/kernel/nucleus/src/api/reply.rs +++ b/kernel/nucleus/src/api/reply.rs @@ -192,3 +192,7 @@ pub fn invoke(cap: &CapEntry, op: u32, arg0: u64, arg1: u64) -> SyscallResult { _ => Err(SyscallError::InvalidOp), } } + +impl KernelObject for Reply { + const TYPE: ObjectType = ObjectType::Reply; +} diff --git a/kernel/nucleus/src/api/time.rs b/kernel/nucleus/src/api/time.rs index 8ef318a87..01058975e 100644 --- a/kernel/nucleus/src/api/time.rs +++ b/kernel/nucleus/src/api/time.rs @@ -103,3 +103,7 @@ pub fn invoke(key: &TimeKey, op: u32, args: [u64; 4]) -> SyscallResult { _ => Err(SyscallError::InvalidOp), } } + +impl KernelObject for TimeSlice { + const TYPE: ObjectType = ObjectType::Time; +} diff --git a/kernel/nucleus/src/api/untyped.rs b/kernel/nucleus/src/api/untyped.rs index b65ab6a05..71e4f551d 100644 --- a/kernel/nucleus/src/api/untyped.rs +++ b/kernel/nucleus/src/api/untyped.rs @@ -287,3 +287,7 @@ fn core_object_size(obj_type: ObjectType, size_bits: u8) -> Result Err(CapError::InvalidObjectType), } } + +impl KernelObject for Untyped { + const TYPE: ObjectType = ObjectType::Untyped; +} diff --git a/kernel/nucleus/src/api/arch_objects.rs b/kernel/nucleus/src/arch_objects.rs similarity index 100% rename from kernel/nucleus/src/api/arch_objects.rs rename to kernel/nucleus/src/arch_objects.rs diff --git a/kernel/nucleus/src/main.rs b/kernel/nucleus/src/main.rs index 0f9d6ef27..65fb83dd3 100644 --- a/kernel/nucleus/src/main.rs +++ b/kernel/nucleus/src/main.rs @@ -39,177 +39,6 @@ fn panicked(info: &PanicInfo) -> ! { libmachine::panic::handler(info) } -// ═══════════════════════════════════════════════════════════════════ -// OBJECT TYPE WITH ARCH BIT -// ═══════════════════════════════════════════════════════════════════ - -/// Object type discriminant with architectural bit. -/// -/// Bit 7 (high bit) indicates architecture-specific type. -/// -/// Layout: -/// -/// Bit 7 Bits 6-0 -/// ───── ──────── -/// 0 Core type (0-127) -/// 1 Arch type (0-127) -/// -#[repr(transparent)] -#[derive(Copy, Clone, Debug, PartialEq, Eq)] -pub struct ObjectType(u8); - -impl ObjectType { - /// Bit indicating architecture-specific capability - pub const ARCH_BIT: u8 = 0x80; - - // ─── Core Types (0x00 - 0x7F) ─── - pub const NULL: Self = Self(0); - pub const UNTYPED: Self = Self(1); - pub const DOMAIN: Self = Self(2); - pub const KEY_TABLE: Self = Self(3); - pub const NOTIFICATION: Self = Self(4); - pub const EVENT_COUNT: Self = Self(5); - pub const ENDPOINT: Self = Self(6); - pub const TIME: Self = Self(7); - pub const BUFFER: Self = Self(8); - pub const REPLY: Self = Self(9); - // Reserved: 10-127 - - // ─── Arch Types (0x80 - 0xFF) ─── - pub const FRAME: Self = Self(Self::ARCH_BIT | 0); - pub const PAGE_TABLE: Self = Self(Self::ARCH_BIT | 1); - pub const VSPACE: Self = Self(Self::ARCH_BIT | 2); - pub const ASID_POOL: Self = Self(Self::ARCH_BIT | 3); - pub const ASID: Self = Self(Self::ARCH_BIT | 4); - pub const IO_SPACE: Self = Self(Self::ARCH_BIT | 5); - pub const IO_PORT: Self = Self(Self::ARCH_BIT | 6); // x86 only - pub const IRQ_HANDLER: Self = Self(Self::ARCH_BIT | 7); - pub const IRQ_CONTROL: Self = Self(Self::ARCH_BIT | 8); - // Reserved: 0x89 - 0xFF - - /// Check if this is an architecture-specific type. - #[inline(always)] - pub const fn is_arch(&self) -> bool { - (self.0 & Self::ARCH_BIT) != 0 - } - - /// Check if this is a core type. - #[inline(always)] - pub const fn is_core(&self) -> bool { - (self.0 & Self::ARCH_BIT) == 0 - } - - /// Get the type index within its category (strips arch bit). - #[inline(always)] - pub const fn index(&self) -> u8 { - self.0 & !Self::ARCH_BIT - } - - /// Raw value - #[inline(always)] - pub const fn as_u8(&self) -> u8 { - self.0 - } -} - -// ═══════════════════════════════════════════════════════════════════ -// CORE TYPE ENUM (FOR MATCH) -// ═══════════════════════════════════════════════════════════════════ - -/// Core object types - used for match dispatch after arch check -#[repr(u8)] -#[derive(Copy, Clone, Debug, PartialEq, Eq)] -pub enum CoreType { - /// No capability - Null = 0, - /// Creates objects (including new key tables) - Untyped = 1, - /// Protection domain - Domain = 2, - /// capability table itself - KeyTable = 3, - /// Time capability - Time = 4, - /// Endpoint capability - Endpoint = 5, - /// Notification endpoint capability - Notification = 6, - /// Event count endpoint capability - EventCount = 7, - /// Shareable buffer capability - Buffer = 8, - Reply = 9, -} - -impl TryFrom for CoreType { - type Error = CapError; - - #[inline] - fn try_from(ot: ObjectType) -> Result { - if ot.is_arch() { - return Err(CapError::NotCoreType); - } - match ot.index() { - 0 => Ok(CoreType::Null), - 1 => Ok(CoreType::Untyped), - 2 => Ok(CoreType::Domain), - 3 => Ok(CoreType::KeyTable), - 4 => Ok(CoreType::Notification), - 5 => Ok(CoreType::EventCount), - 6 => Ok(CoreType::Endpoint), - 7 => Ok(CoreType::Time), - 8 => Ok(CoreType::Buffer), - 9 => Ok(CoreType::Reply), - _ => Err(CapError::UnknownCoreType(ot.index())), - } - } -} - -// ═══════════════════════════════════════════════════════════════════ -// ARCH TYPE ENUM (ARCHITECTURE-SPECIFIC) -// ═══════════════════════════════════════════════════════════════════ - -/// Architecture-specific object types -/// -/// This is defined per-architecture but the indices are the same. -/// The actual struct types differ per architecture. -#[repr(u8)] -#[derive(Copy, Clone, Debug, PartialEq, Eq)] -pub enum ArchType { - Frame = 0, - PageTable = 1, - VSpace = 2, - ASIDPool = 3, - ASID = 4, - IOSpace = 5, - IOPort = 6, - IRQHandler = 7, - IRQControl = 8, -} - -impl TryFrom for ArchType { - type Error = CapError; - - #[inline] - fn try_from(ot: ObjectType) -> Result { - if !ot.is_arch() { - return Err(CapError::NotArchType); - } - match ot.index() { - 0 => Ok(ArchType::Frame), - 1 => Ok(ArchType::PageTable), - 2 => Ok(ArchType::VSpace), - 3 => Ok(ArchType::ASIDPool), - 4 => Ok(ArchType::ASID), - 5 => Ok(ArchType::IOSpace), - 6 => Ok(ArchType::IOPort), - 7 => Ok(ArchType::IRQHandler), - 8 => Ok(ArchType::IRQControl), - _ => Err(CapError::UnknownArchType(ot.index())), - } - } -} - // Syscall handler - exception vector for EL0 synchronous exceptions // (the only other thing nucleus does) #[unsafe(naked)] @@ -282,7 +111,8 @@ fn cap_invoke_handler( "CapInvoke SYSCALL(cap: {cap_slot}, op: {op}) happened, we're at 0x{:016X}", get_pc() ); - return (0, 0, 0); + + handle_cap_invoke(NUCLEUS, cap_slot, op, args) // let cap = current_domain().keytable.lookup(cap_slot)?; // let args = &[arg0, arg1, arg2, arg3, arg4, arg5]; // FIXME temp @@ -305,6 +135,8 @@ fn cap_invoke_handler( // } } +static mut NUCLEUS: Nucleus; + // ═══════════════════════════════════════════════════════════════════ // SYSCALL DISPATCH WITH ARCH OBJECTS // ═══════════════════════════════════════════════════════════════════ @@ -319,29 +151,29 @@ fn cap_invoke_handler( /// 2. Each sub-match has fewer cases #[inline(never)] // Keep as separate function for branch prediction pub fn handle_cap_invoke( - kernel: &mut Kernel, + nucleus: &mut Nucleus, cap_slot: u32, op: u32, args: &[u64; 6], ) -> Result<(u64, u64), CapError> { - let domain = kernel.current_domain_mut()?; + let domain = nucleus.current_domain_mut()?; let slot = KeySlot(cap_slot as u16); let entry = domain.keytable.lookup_mut(slot)?; let obj_type = entry.object_type(); if obj_type.is_arch() { // Architecture-specific dispatch (less common path) - arch_invoke::(kernel, entry, obj_type, op, args) + arch_invoke::(nucleus, entry, obj_type, op, args) } else { // Core dispatch (common path) - core_invoke::(kernel, entry, obj_type, op, args) + core_invoke::(nucleus, entry, obj_type, op, args) } } -/// Core object dispatch - ~10 cases +/// Core object dispatch #[inline(always)] fn core_invoke( - kernel: &mut Kernel, + nucleus: &mut Nucleus, entry: &mut KeyEntry, obj_type: ObjectType, op: u32, @@ -352,57 +184,60 @@ fn core_invoke( match core_type { CoreType::Null => Err(CapError::NullCapability), - CoreType::Untyped => { - let untyped = entry.as_object_mut::()?; - api::untyped::invoke(untyped, entry.rights(), op, args, &mut kernel.pools) - } - - CoreType::Domain => { - let domain = entry.as_object_mut::()?; - api::domain::invoke(domain, entry.rights(), op, args) - } - - CoreType::KeyTable => { - let kt = entry.as_object_mut::()?; - api::keytable::invoke(kt, entry.rights(), op, args) - } - - CoreType::Notification => { - let notify = entry.as_object_mut::()?; - api::notification::invoke(notify, entry.rights(), entry.badge(), op, args) - } - - CoreType::EventCount => { - let ec = entry.as_object_mut::()?; - api::event_count::invoke(ec, entry.rights(), op, args) - } - - CoreType::Endpoint => { - let ep = entry.as_object_mut::()?; - api::endpoint::invoke(ep, entry.rights(), entry.badge(), op, args, kernel) - } - - CoreType::Time => { - let time = entry.as_object_mut::()?; - api::time::invoke(time, entry.rights(), op, args, kernel) - } - - CoreType::Buffer => { - let buf = entry.as_object_mut::()?; - api::buffer::invoke(buf, entry.rights(), op, args) - } - - CoreType::Reply => { - let reply = entry.as_object_mut::()?; - api::reply::invoke(reply, op, args, kernel) - } + // CoreType::Untyped => { + // let untyped = entry.as_object_mut::()?; + // // Untyped::invoke(untyped, ....) + // api::untyped::invoke(untyped, entry.rights(), op, args, &mut nucleus.pools) + // } + CoreType::DebugConsole => { + let debug_console = entry.as_object_mut::()?; + DebugConsole::invoke(debug_console, parampampam) + } // CoreType::Domain => { + // let domain = entry.as_object_mut::()?; + // api::domain::invoke(domain, entry.rights(), op, args) + // } + + // CoreType::KeyTable => { + // let kt = entry.as_object_mut::()?; + // api::keytable::invoke(kt, entry.rights(), op, args) + // } + + // CoreType::Notification => { + // let notify = entry.as_object_mut::()?; + // api::notification::invoke(notify, entry.rights(), entry.badge(), op, args) + // } + + // CoreType::EventCount => { + // let ec = entry.as_object_mut::()?; + // api::event_count::invoke(ec, entry.rights(), op, args) + // } + + // CoreType::Endpoint => { + // let ep = entry.as_object_mut::()?; + // api::endpoint::invoke(ep, entry.rights(), entry.badge(), op, args, nucleus) + // } + + // CoreType::Time => { + // let time = entry.as_object_mut::()?; + // api::time::invoke(time, entry.rights(), op, args, nucleus) + // } + + // CoreType::Buffer => { + // let buf = entry.as_object_mut::()?; + // api::buffer::invoke(buf, entry.rights(), op, args) + // } + + // CoreType::Reply => { + // let reply = entry.as_object_mut::()?; + // api::reply::invoke(reply, op, args, nucleus) + // } } } /// Architecture-specific dispatch - defined per architecture #[inline(always)] fn arch_invoke( - kernel: &mut Kernel, + nucleus: &mut Nucleus, entry: &mut KeyEntry, obj_type: ObjectType, op: u32, @@ -413,22 +248,22 @@ fn arch_invoke( match arch_type { ArchType::Frame => { let frame = entry.as_object_mut::()?; - A::invoke_frame(frame, entry.rights(), op, args, kernel) // or A::Frame::invoke()? + A::invoke_frame(frame, entry.rights(), op, args, nucleus) // or A::Frame::invoke()? } ArchType::PageTable => { let pt = entry.as_object_mut::()?; - A::invoke_page_table(pt, entry.rights(), op, args, kernel) + A::invoke_page_table(pt, entry.rights(), op, args, nucleus) } ArchType::VSpace => { let vspace = entry.as_object_mut::()?; - A::invoke_vspace(vspace, entry.rights(), op, args, kernel) + A::invoke_vspace(vspace, entry.rights(), op, args, nucleus) } ArchType::ASIDPool => { let pool = entry.as_object_mut::()?; - A::invoke_asid_pool(pool, entry.rights(), op, args, kernel) + A::invoke_asid_pool(pool, entry.rights(), op, args, nucleus) } ArchType::ASID => { @@ -438,7 +273,7 @@ fn arch_invoke( ArchType::IOSpace => { // May not be supported on all architectures - A::invoke_io_space(entry, op, args, kernel) + A::invoke_io_space(entry, op, args, nucleus) } ArchType::IOPort => { @@ -454,9 +289,9 @@ fn arch_invoke( } } - ArchType::IRQHandler => A::invoke_irq_handler(entry, op, args, kernel), + ArchType::IRQHandler => A::invoke_irq_handler(entry, op, args, nucleus), - ArchType::IRQControl => A::invoke_irq_control(entry, op, args, kernel), + ArchType::IRQControl => A::invoke_irq_control(entry, op, args, nucleus), } } diff --git a/kernel/nucleus/src/api/kernel_object.rs b/kernel/nucleus/src/nucleus_object.rs similarity index 59% rename from kernel/nucleus/src/api/kernel_object.rs rename to kernel/nucleus/src/nucleus_object.rs index fe7633932..40f4d2b60 100644 --- a/kernel/nucleus/src/api/kernel_object.rs +++ b/kernel/nucleus/src/nucleus_object.rs @@ -77,110 +77,18 @@ use core::ptr::NonNull; // │ │ // └─────────────────────────────────────────────────────────────────────┘ -// ═══════════════════════════════════════════════════════════════════ -// OBJECT TYPE DISCRIMINANT - EXTENDED FOR ARCH TYPES -// ═══════════════════════════════════════════════════════════════════ - -/// Core object types (architecture-independent) -/// -/// These are the same across all architectures. -/// Values 0-63 are reserved for core types. -#[repr(u8)] -#[derive(Copy, Clone, Debug, PartialEq, Eq)] -pub enum ObjectType { - // ─── Core Types (0-15) ─── - Null = 0, - Untyped = 1, - Domain = 2, - KeyTable = 3, - Notification = 4, - EventCount = 5, - Endpoint = 6, - Time = 7, - Buffer = 8, - Reply = 9, - - // Reserved for future core types: 10-15 - - // ─── Architecture-Specific Types (16-63) ─── - // These are defined per-architecture but share the enum space - // so we can have a single ObjectType for dispatch. - /// Physical memory frame (page) - Frame = 16, - /// Page table (any level) - PageTable = 17, - /// Virtual address space root - VSpace = 18, - /// ASID pool (AArch64, RISC-V) - ASIDPool = 19, - /// ASID control (AArch64, RISC-V) - ASID = 20, - /// I/O memory space (SMMU/VT-d) - IOSpace = 21, - /// I/O port range (x86 only) - IOPort = 22, - /// IRQ handler object - IRQHandler = 23, - /// IRQ control (for binding IRQs) - IRQControl = 24, - // Reserved for future arch types: 25-63 -} - -impl ObjectType { - /// Is this a core (architecture-independent) type? - #[inline] - pub const fn is_core(&self) -> bool { - (*self as u8) < 16 - } - - /// Is this an architecture-specific type? - #[inline] - pub const fn is_arch(&self) -> bool { - (*self as u8) >= 16 && (*self as u8) < 64 - } -} - // ═══════════════════════════════════════════════════════════════════ // KERNEL OBJECT TRAIT // ═══════════════════════════════════════════════════════════════════ /// Marker trait for kernel objects - provides type → ObjectType mapping -pub trait KernelObject: Sized + 'static { +pub trait NucleusObject: Sized + 'static { const TYPE: ObjectType; //TODO: add invoke here? // fn invoke(obj: &Self::TYPE, op: u32, args: &[u64]) -> SyscallResult; } -// Implement for each kernel object type -impl KernelObject for Untyped { - const TYPE: ObjectType = ObjectType::Untyped; -} -impl KernelObject for Domain { - const TYPE: ObjectType = ObjectType::Domain; -} -impl KernelObject for KeyTable { - const TYPE: ObjectType = ObjectType::KeyTable; -} -impl KernelObject for Notification { - const TYPE: ObjectType = ObjectType::Notification; -} -impl KernelObject for EventCount { - const TYPE: ObjectType = ObjectType::EventCount; -} -impl KernelObject for Endpoint { - const TYPE: ObjectType = ObjectType::Endpoint; -} -impl KernelObject for TimeSlice { - const TYPE: ObjectType = ObjectType::Time; -} -impl KernelObject for Buffer { - const TYPE: ObjectType = ObjectType::Buffer; -} -impl KernelObject for Reply { - const TYPE: ObjectType = ObjectType::Reply; -} - // ═══════════════════════════════════════════════════════════════════ // TYPE-ERASED OBJECT POINTER // ═══════════════════════════════════════════════════════════════════ @@ -197,7 +105,7 @@ pub struct ObjectRef { impl ObjectRef { /// Create a new object reference from a typed pointer - pub fn new(obj: &T) -> Self { + pub fn new(obj: &T) -> Self { Self { ptr: NonNull::from(obj).cast(), obj_type: T::TYPE, @@ -208,7 +116,7 @@ impl ObjectRef { /// /// # Safety /// Caller must ensure the pointer is valid and properly aligned - pub unsafe fn from_raw(ptr: *mut T) -> Self { + pub unsafe fn from_raw(ptr: *mut T) -> Self { Self { ptr: NonNull::new_unchecked(ptr.cast()), obj_type: T::TYPE, @@ -223,7 +131,7 @@ impl ObjectRef { /// Attempt to cast to a specific type (immutable) #[inline] - pub fn try_as(&self) -> Option<&T> { + pub fn try_as(&self) -> Option<&T> { if self.obj_type == T::TYPE { // SAFETY: We verified the type matches Some(unsafe { self.ptr.cast::().as_ref() }) @@ -234,7 +142,7 @@ impl ObjectRef { /// Attempt to cast to a specific type (mutable) #[inline] - pub fn try_as_mut(&mut self) -> Option<&mut T> { + pub fn try_as_mut(&mut self) -> Option<&mut T> { if self.obj_type == T::TYPE { // SAFETY: We verified the type matches Some(unsafe { self.ptr.cast::().as_mut() }) @@ -245,7 +153,7 @@ impl ObjectRef { /// Cast with error on type mismatch #[inline] - pub fn as_type(&self) -> Result<&T, CapError> { + pub fn as_type(&self) -> Result<&T, CapError> { self.try_as().ok_or(CapError::TypeMismatch { expected: T::TYPE, found: self.obj_type, @@ -254,121 +162,10 @@ impl ObjectRef { /// Cast with error on type mismatch (mutable) #[inline] - pub fn as_type_mut(&mut self) -> Result<&mut T, CapError> { + pub fn as_type_mut(&mut self) -> Result<&mut T, CapError> { self.try_as_mut().ok_or(CapError::TypeMismatch { expected: T::TYPE, found: self.obj_type, }) } } - -// ═══════════════════════════════════════════════════════════════════ -// KEY ENTRY (CAPABILITY TABLE ENTRY) -// ═══════════════════════════════════════════════════════════════════ - -/// A single entry in a domain's capability table (KeyTable). -/// -/// Size: 32 bytes (fits nicely in cache) -/// -/// ```text -/// ┌────────────────────────────────────────┐ -/// │ object_ref: ObjectRef (16 bytes) │ -/// │ - ptr: NonNull<()> (8 bytes) │ -/// │ - obj_type: ObjectType (1 byte) │ -/// │ - padding (7 bytes) │ -/// ├────────────────────────────────────────┤ -/// │ rights: Rights (2 bytes) │ -/// │ parent_slot: u16 (2 bytes) │ -/// │ badge: u32 (4 bytes) │ -/// │ gen: u32 (4 bytes) │ -/// │ padding (4 bytes) │ -/// └────────────────────────────────────────┘ -/// ``` -#[repr(C)] -pub struct KeyEntry { - /// Reference to the kernel object - object_ref: ObjectRef, - /// Access rights for this capability - rights: Rights, - /// Slot of parent capability (for revocation tree) - /// 0xFFFF = no parent (root capability) - parent_slot: u16, - /// Badge value (for endpoint discrimination, buffer offset, etc.) - badge: u32, - /// Generation counter (detect stale capabilities) - generation: u32, - _pad: u32, -} - -impl KeyEntry { - /// Create a null/empty entry - pub const fn null() -> Self { - Self { - object_ref: ObjectRef { - ptr: NonNull::dangling(), - obj_type: ObjectType::Null, - }, - rights: Rights::empty(), - parent_slot: 0xFFFF, - badge: 0, - generation: 0, - _pad: 0, - } - } - - /// Create a new capability entry - pub fn new( - object: &T, - rights: Rights, - badge: u32, - parent: Option, - ) -> Self { - Self { - object_ref: ObjectRef::new(object), - rights, - parent_slot: parent.map(|s| s.0).unwrap_or(0xFFFF), - badge, - generation: 0, - _pad: 0, - } - } - - /// Check if this entry is valid (not null) - #[inline] - pub fn is_valid(&self) -> bool { - self.object_ref.obj_type != ObjectType::Null - } - - /// Get the object type - #[inline] - pub fn object_type(&self) -> ObjectType { - self.object_ref.obj_type - } - - /// Get access rights - #[inline] - pub fn rights(&self) -> Rights { - self.rights - } - - /// Get badge value - #[inline] - pub fn badge(&self) -> u32 { - self.badge - } - - /// Access the underlying object with type checking - #[inline] - pub fn as_object(&self) -> Result<&T, CapError> { - self.object_ref.as_type() - } - - /// Access the underlying object mutably with type checking - #[inline] - pub fn as_object_mut(&mut self) -> Result<&mut T, CapError> { - self.object_ref.as_type_mut() - } -} - -// Verify size at compile time -const _: () = assert!(core::mem::size_of::() == 32); diff --git a/kernel/nucleus/src/api/object_pool.rs b/kernel/nucleus/src/object_pool.rs similarity index 96% rename from kernel/nucleus/src/api/object_pool.rs rename to kernel/nucleus/src/object_pool.rs index 4deff55c3..9ca4c752b 100644 --- a/kernel/nucleus/src/api/object_pool.rs +++ b/kernel/nucleus/src/object_pool.rs @@ -2,6 +2,8 @@ // OBJECT POOLS // ═══════════════════════════════════════════════════════════════════ +// FIXME: allocate a whole pool (X objects of same type) via untyped retype and then get objects from pool as needed + /// A pool of kernel objects of type T, backed by untyped memory. /// /// Objects are allocated via Untyped.Retype and live until revoked. From 657bf664dce7c88988598e225cf9496a1e2e47ed Mon Sep 17 00:00:00 2001 From: Berkus Decker Date: Fri, 30 Jan 2026 01:15:30 +0200 Subject: [PATCH 054/107] refactor: #2 - untypeds --- .../nucleus/src/api/arch/aarch64_objects.rs | 3 +- kernel/nucleus/src/api/arch/mod.rs | 2 + kernel/nucleus/src/api/key_entry.rs | 3 + kernel/nucleus/src/api/mod.rs | 4 +- kernel/nucleus/src/api/untyped.rs | 168 ++++++++++++++++-- kernel/nucleus/src/main.rs | 12 +- kernel/nucleus/src/nucleus.rs | 8 +- kernel/nucleus/src/object_pool.rs | 4 +- 8 files changed, 174 insertions(+), 30 deletions(-) create mode 100644 kernel/nucleus/src/api/arch/mod.rs diff --git a/kernel/nucleus/src/api/arch/aarch64_objects.rs b/kernel/nucleus/src/api/arch/aarch64_objects.rs index 91d1fed00..2d9e32e29 100644 --- a/kernel/nucleus/src/api/arch/aarch64_objects.rs +++ b/kernel/nucleus/src/api/arch/aarch64_objects.rs @@ -2,7 +2,8 @@ // AARCH64 IMPLEMENTATION // ═══════════════════════════════════════════════════════════════════ -#[cfg(target_arch = "aarch64")] +struct AArch64; + impl ArchObjects for AArch64 { type Frame = AArch64Frame; type PageTable = AArch64PageTable; diff --git a/kernel/nucleus/src/api/arch/mod.rs b/kernel/nucleus/src/api/arch/mod.rs new file mode 100644 index 000000000..875b33cf5 --- /dev/null +++ b/kernel/nucleus/src/api/arch/mod.rs @@ -0,0 +1,2 @@ +#[cfg(target_arch = "aarch64")] +mod aarch64_objects; diff --git a/kernel/nucleus/src/api/key_entry.rs b/kernel/nucleus/src/api/key_entry.rs index 125085a06..113cd8874 100644 --- a/kernel/nucleus/src/api/key_entry.rs +++ b/kernel/nucleus/src/api/key_entry.rs @@ -23,6 +23,9 @@ pub struct KeyEntry { /// Reference to the kernel object object_ref: ObjectRef, + // FIXME: OR, + /// Physical address of the kernel object + // object_paddr: PhysAddr, /// Access rights for this capability rights: Rights, /// Slot of parent capability (for revocation tree) diff --git a/kernel/nucleus/src/api/mod.rs b/kernel/nucleus/src/api/mod.rs index 2eff4f7bd..2dc4c4f5c 100644 --- a/kernel/nucleus/src/api/mod.rs +++ b/kernel/nucleus/src/api/mod.rs @@ -1,6 +1,4 @@ -// TODO: move these api decls/impls to libraries in libs/, re-export only user part through -// TODO: libsyscall or something like that - usable from userspace. - +mod arch; mod debug_console; mod key; mod nucleus_object; diff --git a/kernel/nucleus/src/api/untyped.rs b/kernel/nucleus/src/api/untyped.rs index 71e4f551d..b56c3eea4 100644 --- a/kernel/nucleus/src/api/untyped.rs +++ b/kernel/nucleus/src/api/untyped.rs @@ -10,10 +10,25 @@ pub struct UntypedKey { enum RetypeError {} +// ┌─────────────────────────────────────────────────────────────────┐ +// │ ALLOWED OPERATIONS ON UNTYPED │ +// ├─────────────────────────────────────────────────────────────────┤ +// │ ✓ seL4_Untyped_Retype → Create children (objects/sub-untypeds)│ +// │ ✓ seL4_CNode_Revoke → Delete all children, reset watermark │ +// │ ✓ seL4_CNode_Delete → Delete this cap (if no children) │ +// │ ✓ seL4_CNode_Move → Move cap to different slot │ +// ├─────────────────────────────────────────────────────────────────┤ +// │ DISALLOWED │ +// ├─────────────────────────────────────────────────────────────────┤ +// │ ✗ seL4_CNode_Copy → Cannot duplicate │ +// │ ✗ seL4_CNode_Mint → Cannot derive with reduced rights │ +// │ ✗ seL4_CNode_Mutate → Cannot modify │ +// └─────────────────────────────────────────────────────────────────┘ + impl UntypedKey { - /// Retype untyped memory into a typed kernel object. + /// Retype untyped memory into a typed nucleus object. /// - /// This is how ALL kernel objects are created (seL4 pattern). + /// This is how ALL nucleus objects are created (seL4 pattern). /// The untyped capability is consumed/reduced by the operation. pub fn retype( &self, @@ -34,11 +49,89 @@ impl UntypedKey { } } -// ============================================== -// == Kernel space object and syscall handling == -// ============================================== +// Errors that can occur during retype operation +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[repr(u32)] +pub enum RetypeError { + /// Invalid object type specified + InvalidObjectType = 1, + /// Requested size is invalid for object type + InvalidSize = 2, + /// Not enough memory remaining in untyped + InsufficientMemory = 3, + /// Alignment requirements not met + AlignmentError = 4, + /// Destination slot already occupied + SlotOccupied = 5, + /// Invalid destination slot + InvalidSlot = 6, + /// Untyped has already been retyped (has children) + AlreadyRetyped = 7, + /// Object type requires specific size_bits + SizeMismatch = 8, + /// Maximum number of objects reached + ObjectLimitReached = 9, + /// Internal kernel error + InternalError = 10, +} -struct Untyped; +impl RetypeError { + pub fn code(self) -> u32 { + self as u32 + } +} + +// =============================================== +// == Nucleus space object and syscall handling == +// =============================================== + +// ┌─────────────────────────────────────────────────────────────────────┐ +// │ cap_untyped_cap (64-bit systems) │ +// ├──────────────────┬──────────────────────────────────────────────────┤ +// │ capType (4 bits)│ = cap_untyped_cap │ +// ├──────────────────┼──────────────────────────────────────────────────┤ +// │ capPtr │ Physical address of untyped region │ +// ├──────────────────┼──────────────────────────────────────────────────┤ +// │ capBlockSize │ Total size in bits (region = 2^capBlockSize) │ +// ├──────────────────┼──────────────────────────────────────────────────┤ +// │ capFreeIndex │ Watermark: next free byte offset (>> MIN_BITS) │ +// ├──────────────────┼──────────────────────────────────────────────────┤ +// │ capIsDevice │ Boolean: is this device memory (not normal RAM)? │ +// └──────────────────┴──────────────────────────────────────────────────┘ + +// The key insight: an Untyped capability IS the kernel object. +// There's no separate in-kernel structure — just the capability itself carrying all the metadata. +// This is extremely minimal! + +/// State of an untyped memory region - FIXME prolly not needed +#[repr(u8)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum UntypedState { + /// Fresh, no children created + Free = 0, + /// Has been split into children + HasChildren = 1, + /// Has been retyped to a typed object + Retyped = 2, +} + +/// Represents a region of physical memory that can be retyped +#[repr(C, align(64))] +pub struct UntypedObject { + /// Physical base address of the memory region + pub paddr: PhysAddr, + /// Size as log2 (actual size = 1 << size_bits) + pub size_bits: u8, + /// Current state + pub state: UntypedState, + /// Number of child untypeds (if split) + pub child_count: u16, + /// Watermark: offset of next free byte within region + /// Only valid when state == Free + pub watermark: u64, +} + +const _: () = assert!(size_of::() <= 64); #[repr(u8)] enum UntypedOp { @@ -85,7 +178,7 @@ mod untyped_tests { #[test] fn create_domain() { - // (~4KB typically, includes kernel stack + metadata) + // (~4KB typically, includes nucleus stack + metadata) untyped_retype(mem, ObjectType::Domain, 12, slot_c)?; let domain = DomainCap::from_slot(slot_c); } @@ -102,8 +195,59 @@ mod untyped_tests { // EXTENDED RETYPE WITH ARCH OBJECTS // ═══════════════════════════════════════════════════════════════════ +// UNTYPED REGION (2^capBlockSize bytes) +// ┌────────────────────────────────────────────────────────────┐ +// │█████████████████████████░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░│ +// │ ALLOCATED │ FREE │ +// └────────────────────────────────────────────────────────────┘ +// ↑ ↑ ↑ +// capPtr capPtr + FREE_INDEX_TO_OFFSET(capFreeIndex) capPtr + 2^capBlockSize +// (watermark) + +// VALIDATION: +// Type valid? Size within bounds? +// Device untyped → only Frames/Untypeds allowed +// num ≤ CONFIG_RETYPE_FAN_OUT_LIMIT (default 256)? +// Destination slots empty? +// +// CALCULATE ALIGNED FREE POINTER +// +// freeRef = GET_FREE_REF(capPtr, freeIndex) +// objectSize = 1 << size_bits +// alignedFreeRef = ROUND_UP(freeRef, objectSize) ← CRITICAL! +// +// (alignment can waste memory — the "fragmentation" issue) +// +// CHECK SUFFICIENT SPACE +// +// untypedFreeBytes = regionEnd - alignedFreeRef +// if (untypedFreeBytes >> objectSizeBits) < num: +// return seL4_NotEnoughMemory (with memoryLeft in MR) +// +// RESET CHECK (if children were revoked) +// +// if (cap_untyped_cap_get_capFreeIndex(cap) != 0 +// && children_revoked): +// • Zero memory (preemptible!) +// • Reset capFreeIndex to 0 +// +// CREATE OBJECTS +// +// for i in 0..num: +// objectAddr = alignedFreeRef + (i * objectSize) +// cap = createObject(type, objectAddr, size_bits, ...) +// insert_cap_into_cnode(destSlots[i], cap) +// establish_CDT_relationship(untypedCap → newCap) +// +// UPDATE WATERMARK +// +// newFreeIndex = OFFSET_TO_FREE_INDEX( +// alignedFreeRef + num*objectSize - capPtr +// ) +// cap_untyped_cap_set_capFreeIndex(cap, newFreeIndex) + impl Untyped { - /// Retype this untyped memory into a kernel object. + /// Retype this untyped memory into a nucleus object. /// /// Handles both core and architecture-specific object types. pub fn retype( @@ -112,7 +256,7 @@ impl Untyped { size_bits: u8, dest_slot: KeySlot, dest_keytable: &mut KeyTable, - pools: &mut KernelPools, + pools: &mut NucleusPools, ) -> Result<(), CapError> { // Determine object size based on type let obj_size = if obj_type.is_core() { @@ -148,7 +292,7 @@ impl Untyped { obj_type: ObjectType, phys_addr: PhysAddr, size_bits: u8, - pools: &mut KernelPools, + pools: &mut NucleusPools, ) -> Result { match obj_type { ObjectType::Notification => { @@ -250,7 +394,7 @@ impl Untyped { obj_type: ObjectType, phys_addr: PhysAddr, size_bits: u8, - pools: &mut KernelPools, + pools: &mut NucleusPools, ) -> Result { let obj_ref = A::create_arch_object(obj_type, phys_addr, size_bits, &mut pools.arch)?; @@ -288,6 +432,6 @@ fn core_object_size(obj_type: ObjectType, size_bits: u8) -> Result; +/// Global kernel state, protected by The Great Kernel Lock +static mut NUCLEUS: IRQSafeNullLock>> = IRQSafeNullLock::new(LazyLock::new(|| Nucleus)); // ═══════════════════════════════════════════════════════════════════ // SYSCALL DISPATCH WITH ARCH OBJECTS diff --git a/kernel/nucleus/src/nucleus.rs b/kernel/nucleus/src/nucleus.rs index 8d0a45b35..527cc205c 100644 --- a/kernel/nucleus/src/nucleus.rs +++ b/kernel/nucleus/src/nucleus.rs @@ -32,7 +32,7 @@ // ═══════════════════════════════════════════════════════════════════ /// All kernel object pools - both core and architecture-specific -pub struct KernelPools { +pub struct NucleusPools { // ─── Core Object Pools ─── pub untypeds: ObjectPool, pub domains: ObjectPool, @@ -48,12 +48,12 @@ pub struct KernelPools { pub arch: ArchPools, } -/// Complete kernel state (parameterized by architecture) +/// Complete nucleus state (parameterized by architecture) pub struct Nucleus { /// All object pools - pub pools: KernelPools, + pub pools: NucleusPools, /// Currently running domain - pub current_domain: Option, + pub current_domain: Option, // FIXME: not option, always something (Idle or other) /// DCB shared pages pub dcb_pages: DcbPages, } diff --git a/kernel/nucleus/src/object_pool.rs b/kernel/nucleus/src/object_pool.rs index 9ca4c752b..3dde66489 100644 --- a/kernel/nucleus/src/object_pool.rs +++ b/kernel/nucleus/src/object_pool.rs @@ -7,7 +7,7 @@ /// A pool of kernel objects of type T, backed by untyped memory. /// /// Objects are allocated via Untyped.Retype and live until revoked. -pub struct ObjectPool { +pub struct ObjectPool { /// Base address of the pool base: *mut T, /// Bitmap of allocated slots @@ -18,7 +18,7 @@ pub struct ObjectPool { capacity: u16, } -impl ObjectPool { +impl ObjectPool { /// Create a new pool backed by untyped memory /// /// # Safety From ca3a7b2f5a4522f3f7c2e3be481857700b9a1df9 Mon Sep 17 00:00:00 2001 From: Berkus Decker Date: Fri, 30 Jan 2026 16:29:23 +0200 Subject: [PATCH 055/107] wip: add libobject - split into user-space part, invocation handler and kernel object impl - refactor libsyscall to be plain - add Rights type --- .zed/settings.json | 31 +- Cargo.toml | 2 + kernel/init_thread/src/main.rs | 3 +- kernel/nucleus/Cargo.toml | 2 + kernel/nucleus/design.md | 10 +- kernel/nucleus/src/api/arch/asid_pool.rs | 6 - kernel/nucleus/src/api/arch/frame.rs | 52 +- kernel/nucleus/src/api/arch/mod.rs | 3 +- kernel/nucleus/src/api/arch/page_table.rs | 24 +- kernel/nucleus/src/api/arch/vspace.rs | 26 +- kernel/nucleus/src/api/buffer.rs | 227 +-------- kernel/nucleus/src/api/debug_console.rs | 54 +-- kernel/nucleus/src/api/domain.rs | 341 ------------- kernel/nucleus/src/api/endpoint.rs | 449 +----------------- kernel/nucleus/src/api/event_count.rs | 88 ---- kernel/nucleus/src/api/key_entry.rs | 12 +- kernel/nucleus/src/api/key_table.rs | 242 +--------- kernel/nucleus/src/api/mod.rs | 174 ++++++- kernel/nucleus/src/api/notification.rs | 149 ++---- kernel/nucleus/src/api/reply.rs | 165 ------- kernel/nucleus/src/api/time.rs | 83 ---- kernel/nucleus/src/api/untyped.rs | 386 +-------------- kernel/nucleus/src/main.rs | 189 +------- .../{api => objects}/arch/aarch64_objects.rs | 186 +------- .../src/{api => objects}/arch/arch_pools.rs | 0 kernel/nucleus/src/objects/arch/frame.rs | 50 ++ kernel/nucleus/src/objects/arch/mod.rs | 9 + kernel/nucleus/src/objects/arch/page_table.rs | 1 + .../nucleus/src/{ => objects}/arch_objects.rs | 12 +- kernel/nucleus/src/objects/buffer.rs | 33 ++ kernel/nucleus/src/objects/debug_console.rs | 25 + kernel/nucleus/src/objects/domain.rs | 245 ++++++++++ kernel/nucleus/src/objects/endpoint.rs | 196 ++++++++ kernel/nucleus/src/objects/event_count.rs | 12 + kernel/nucleus/src/objects/key_table.rs | 100 ++++ kernel/nucleus/src/objects/mod.rs | 6 + kernel/nucleus/src/objects/notification.rs | 22 + kernel/nucleus/src/{ => objects}/nucleus.rs | 0 .../src/{ => objects}/nucleus_object.rs | 85 +--- .../nucleus/src/{ => objects}/object_pool.rs | 0 kernel/nucleus/src/objects/object_ref.rs | 85 ++++ kernel/nucleus/src/objects/reply.rs | 70 +++ kernel/nucleus/src/objects/time.rs | 13 + kernel/nucleus/src/objects/untyped.rs | 297 ++++++++++++ libs/object/Cargo.toml | 25 + libs/object/src/arch/asid_pool.rs | 5 + libs/object/src/arch/frame.rs | 11 + libs/object/src/arch/page_table.rs | 7 + libs/object/src/arch/vspace.rs | 11 + libs/object/src/buffer.rs | 191 ++++++++ libs/object/src/debug_console.rs | 32 ++ libs/object/src/domain.rs | 91 ++++ libs/object/src/endpoint.rs | 252 ++++++++++ libs/object/src/event_count.rs | 73 +++ .../src/api => libs/object/src}/key.rs | 4 +- libs/object/src/key_table.rs | 115 +++++ libs/object/src/lib.rs | 45 ++ libs/object/src/notification.rs | 45 ++ .../api => libs/object/src}/object_type.rs | 6 +- libs/object/src/reply.rs | 93 ++++ libs/object/src/rights.rs | 18 + libs/object/src/time.rs | 66 +++ libs/object/src/untyped.rs | 85 ++++ libs/syscall/src/lib.rs | 253 +++++----- 64 files changed, 2833 insertions(+), 2760 deletions(-) rename kernel/nucleus/src/{api => objects}/arch/aarch64_objects.rs (53%) rename kernel/nucleus/src/{api => objects}/arch/arch_pools.rs (100%) create mode 100644 kernel/nucleus/src/objects/arch/frame.rs create mode 100644 kernel/nucleus/src/objects/arch/mod.rs create mode 100644 kernel/nucleus/src/objects/arch/page_table.rs rename kernel/nucleus/src/{ => objects}/arch_objects.rs (92%) create mode 100644 kernel/nucleus/src/objects/buffer.rs create mode 100644 kernel/nucleus/src/objects/debug_console.rs create mode 100644 kernel/nucleus/src/objects/domain.rs create mode 100644 kernel/nucleus/src/objects/endpoint.rs create mode 100644 kernel/nucleus/src/objects/event_count.rs create mode 100644 kernel/nucleus/src/objects/key_table.rs create mode 100644 kernel/nucleus/src/objects/mod.rs create mode 100644 kernel/nucleus/src/objects/notification.rs rename kernel/nucleus/src/{ => objects}/nucleus.rs (100%) rename kernel/nucleus/src/{ => objects}/nucleus_object.rs (74%) rename kernel/nucleus/src/{ => objects}/object_pool.rs (100%) create mode 100644 kernel/nucleus/src/objects/object_ref.rs create mode 100644 kernel/nucleus/src/objects/reply.rs create mode 100644 kernel/nucleus/src/objects/time.rs create mode 100644 kernel/nucleus/src/objects/untyped.rs create mode 100644 libs/object/Cargo.toml create mode 100644 libs/object/src/arch/asid_pool.rs create mode 100644 libs/object/src/arch/frame.rs create mode 100644 libs/object/src/arch/page_table.rs create mode 100644 libs/object/src/arch/vspace.rs create mode 100644 libs/object/src/buffer.rs create mode 100644 libs/object/src/debug_console.rs create mode 100644 libs/object/src/domain.rs create mode 100644 libs/object/src/endpoint.rs create mode 100644 libs/object/src/event_count.rs rename {kernel/nucleus/src/api => libs/object/src}/key.rs (86%) create mode 100644 libs/object/src/key_table.rs create mode 100644 libs/object/src/lib.rs create mode 100644 libs/object/src/notification.rs rename {kernel/nucleus/src/api => libs/object/src}/object_type.rs (97%) create mode 100644 libs/object/src/reply.rs create mode 100644 libs/object/src/rights.rs create mode 100644 libs/object/src/time.rs create mode 100644 libs/object/src/untyped.rs diff --git a/.zed/settings.json b/.zed/settings.json index 42597fe60..1b5b8b199 100644 --- a/.zed/settings.json +++ b/.zed/settings.json @@ -2,17 +2,34 @@ "lsp": { "rust-analyzer": { "initialization_options": { + "rust": { + "analyzerTargetDir": true, + }, + "checkOnSave": { + "enable": true, + "command": "clippy", + "target": "/Users/berkus/Projects/Metta/vesper/targets/aarch64-metta-none-eabi.json", // $ZED_WORKTREE_ROOT + "extraArgs": [ + "-Zbuild-std=compiler_builtins,core,alloc", + "-Zbuild-std-features=compiler-builtins-mem", + "-Zmacro-backtrace", + "--cfg board_rpi3", + "-Ctarget-cpu=cortex-a53", + "--features qemu", + ], + }, "cargo": { "target": "/Users/berkus/Projects/Metta/vesper/targets/aarch64-metta-none-eabi.json", // $ZED_WORKTREE_ROOT + "allTargets": false, "extraArgs": [ "-Zbuild-std=compiler_builtins,core,alloc", - "-Zbuild-std-features=compiler-builtins-mem" - ] + "-Zbuild-std-features=compiler-builtins-mem", + ], }, "rustfmt": { - "extraArgs": ["+nightly"] - } - } - } - } + "extraArgs": ["+nightly"], + }, + }, + }, + }, } diff --git a/Cargo.toml b/Cargo.toml index 1990eb370..6369697bc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,6 +16,7 @@ members = [ "libs/log", "libs/machine", "libs/memory", + "libs/object", "libs/platform", "libs/primitives", "libs/print", @@ -58,6 +59,7 @@ liblocking = { path = "libs/locking" } liblog = { path = "libs/log" } libmachine = { path = "libs/machine" } libmemory = { path = "libs/memory" } +libobject = { path = "libs/object" } libplatform = { path = "libs/platform" } libprimitives = { path = "libs/primitives" } libprint = { path = "libs/print" } diff --git a/kernel/init_thread/src/main.rs b/kernel/init_thread/src/main.rs index 0ea6d076c..cd3cea6e9 100644 --- a/kernel/init_thread/src/main.rs +++ b/kernel/init_thread/src/main.rs @@ -42,7 +42,6 @@ mod embed; mod loader; mod memory; mod paging; -mod syscall_test; use { crate::boot_info::{BOOT_INFO, BootInfoMemRegion}, @@ -57,8 +56,8 @@ use { liblocking::interface::Mutex, libmemory::{phys_addr::PhysAddr, virt_addr::VirtAddr}, libqemu::semi_println, + libsyscall::protected_call6, memory::BootAllocator, - syscall_test::protected_call6, }; unsafe extern "C" { diff --git a/kernel/nucleus/Cargo.toml b/kernel/nucleus/Cargo.toml index 8a1396852..fa3d591db 100644 --- a/kernel/nucleus/Cargo.toml +++ b/kernel/nucleus/Cargo.toml @@ -40,9 +40,11 @@ libconsole = { workspace = true } libcpu = { workspace = true } libexception = { workspace = true } libkernel-state = { workspace = true } +liblocking = { workspace = true } liblog = { workspace = true } libmachine = { workspace = true } libmemory = { workspace = true } +libobject = { workspace = true } libplatform = { workspace = true } libprint = { workspace = true } libqemu = { workspace = true, optional = true } diff --git a/kernel/nucleus/design.md b/kernel/nucleus/design.md index 1d91c22ff..28ac132ce 100644 --- a/kernel/nucleus/design.md +++ b/kernel/nucleus/design.md @@ -1,4 +1,4 @@ -Key Design Points +Key Design Points: Aspect | Design Choice | Rationale Type numbering | Core 0-15, Arch 16-63 | Clear separation, room for growth @@ -13,3 +13,11 @@ Kernel API surface: Wait options (open wait, like servers waiting; closed wait, like waiting one client's response specifically) Timeouts for send and recv phases separately + +Structure: + +Key - a capability index in the keytable - this key is per-domain because it indexes a local domain table +KeyEntry - a capability "value" - entry in the keytable, capabilities are same-size values carrying type, + pointer to kernel object and access rights. +A kernel object corresponding to the capability is allocated in the object pool. +Untyped does not have an "object", it points to memory region to allocate from, and amount of allocations already done. diff --git a/kernel/nucleus/src/api/arch/asid_pool.rs b/kernel/nucleus/src/api/arch/asid_pool.rs index 29313083c..fbc8a70b9 100644 --- a/kernel/nucleus/src/api/arch/asid_pool.rs +++ b/kernel/nucleus/src/api/arch/asid_pool.rs @@ -1,9 +1,3 @@ -#[repr(u8)] -pub enum ASIDPoolOp { - /// Allocate an ASID from this pool - Allocate = 0, -} - pub fn invoke( pool: &mut A::ASIDPool, rights: Rights, diff --git a/kernel/nucleus/src/api/arch/frame.rs b/kernel/nucleus/src/api/arch/frame.rs index 3eee60077..66841e6bc 100644 --- a/kernel/nucleus/src/api/arch/frame.rs +++ b/kernel/nucleus/src/api/arch/frame.rs @@ -1,14 +1,6 @@ -#[repr(u8)] -pub enum FrameOp { - /// Map frame into a VSpace at given virtual address - Map = 0, - /// Unmap frame from VSpace - Unmap = 1, - /// Get physical address (requires special rights) - GetAddress = 2, - /// Remap with different attributes - Remap = 3, -} +use libobject::arch::frame::FrameOp; + +// pub trait FrameInvoke { fn invoke() } pub fn invoke( frame: &mut A::Frame, @@ -22,25 +14,49 @@ pub fn invoke( FrameOp::Map => { // args[0] = vspace_slot // args[1] = virt_addr - // args[2] = attrs (R/W/X) + // args[2] = rights (R/W/X bits) + // args[3] = attrs (cacheability, etc.) + if !rights.contains(Rights::READ) { return Err(CapError::InsufficientRights); } - // Implementation depends on A::Frame - todo!("frame map") + + let vspace_slot = KeySlot(args[0] as u16); + let virt_addr = VirtAddr::new(args[1]); + let map_rights = MapRights::from_bits(args[2] as u8); + let attrs = MemAttrs::from_bits(args[3] as u8); + + // Get the VSpace from the slot + let domain = kernel.current_domain()?; + let vspace_entry = domain.keytable.lookup(vspace_slot)?; + let vspace = vspace_entry.as_object::()?; + + // Perform the mapping + // aarch64_map_frame(frame, vspace, virt_addr, map_rights, attrs, kernel)?; + + Ok((0, 0)) } + FrameOp::Unmap => { - todo!("frame unmap") + if frame.map_count == 0 { + return Err(CapError::NotMapped); + } + // ... unmap logic + todo!("frame unmap"); + Ok((0, 0)) } + FrameOp::GetAddress => { + // Requires Grant right to expose physical address if !rights.contains(Rights::GRANT) { return Err(CapError::InsufficientRights); } - // Return physical address - todo!("frame get_address") + Ok((frame.phys_addr.as_u64(), frame.size.size() as u64)) } + FrameOp::Remap => { - todo!("frame remap") + // Change attributes on existing mapping + todo!("frame remap"); } } } diff --git a/kernel/nucleus/src/api/arch/mod.rs b/kernel/nucleus/src/api/arch/mod.rs index 875b33cf5..870cc1ec5 100644 --- a/kernel/nucleus/src/api/arch/mod.rs +++ b/kernel/nucleus/src/api/arch/mod.rs @@ -1,2 +1 @@ -#[cfg(target_arch = "aarch64")] -mod aarch64_objects; +pub mod frame; diff --git a/kernel/nucleus/src/api/arch/page_table.rs b/kernel/nucleus/src/api/arch/page_table.rs index c68d43022..609821852 100644 --- a/kernel/nucleus/src/api/arch/page_table.rs +++ b/kernel/nucleus/src/api/arch/page_table.rs @@ -1,11 +1,3 @@ -#[repr(u8)] -pub enum PageTableOp { - /// Map a page table into parent table - Map = 0, - /// Unmap from parent - Unmap = 1, -} - pub fn invoke( pt: &mut A::PageTable, rights: Rights, @@ -13,5 +5,19 @@ pub fn invoke( args: &[u64; 6], kernel: &mut Kernel, ) -> Result<(u64, u64), CapError> { - todo!("page_table invoke") + match op { + PageTableOp::Map => { + // args[0] = vspace_slot + // args[1] = virt_addr (determines which slot in parent) + let vspace_slot = KeySlot(args[0] as u16); + let virt_addr = VirtAddr::new(args[1]); + + // ... mapping logic + todo!("page_table map") + } + + PageTableOp::Unmap => { + todo!("page_table unmap") + } + } } diff --git a/kernel/nucleus/src/api/arch/vspace.rs b/kernel/nucleus/src/api/arch/vspace.rs index 37b46f5af..790b8f566 100644 --- a/kernel/nucleus/src/api/arch/vspace.rs +++ b/kernel/nucleus/src/api/arch/vspace.rs @@ -1,15 +1,3 @@ -#[repr(u8)] -pub enum VSpaceOp { - /// Assign root page table - SetRoot = 0, - /// Assign ASID - AssignASID = 1, - /// Activate (switch to this address space) - Activate = 2, - /// Get current ASID - GetASID = 3, -} - pub fn invoke( vspace: &mut A::VSpace, rights: Rights, @@ -26,13 +14,23 @@ pub fn invoke( } VSpaceOp::AssignASID => { // args[0] = asid_pool_slot - todo!("vspace assign_asid") + let pool_slot = KeySlot(args[0] as u16); + + let domain = kernel.current_domain_mut()?; + let pool_entry = domain.keytable.lookup_mut(pool_slot)?; + let pool = pool_entry.as_object_mut::()?; + + let asid = pool.allocate().ok_or(CapError::ASIDPoolExhausted)?; + + vspace.asid = Some(asid); + Ok((asid as u64, 0)) } VSpaceOp::Activate => { todo!("vspace activate") } VSpaceOp::GetASID => { - todo!("vspace get_asid") + let asid = vspace.asid.ok_or(CapError::NoASIDAssigned)?; + Ok((asid as u64, 0)) } } } diff --git a/kernel/nucleus/src/api/buffer.rs b/kernel/nucleus/src/api/buffer.rs index 2f2d2e7ee..9094cee99 100644 --- a/kernel/nucleus/src/api/buffer.rs +++ b/kernel/nucleus/src/api/buffer.rs @@ -1,229 +1,8 @@ -// ================================================== -// == Public user interface, usable from userspace == -// ================================================== - -/// Buffer capability with permission tracking in type system -pub struct BufferKey { - key: Key, - size: usize, - _perm: PhantomData

, -} - -pub trait Permission {} -pub struct ReadOnly; -pub struct ReadWrite; -impl Permission for ReadOnly {} -impl Permission for ReadWrite {} - -impl BufferKey

{ - /// Map buffer into current domain's address space. - /// - /// # Arguments - /// * `hint` - Optional preferred virtual address (kernel may ignore) - /// * `flags` - Mapping flags (caching behavior, etc.) - /// - /// # Returns - /// Virtual address where buffer is mapped - pub fn map(&self, hint: Option, flags: MapFlags) -> Result { - let hint_val = hint.map(|a| a.as_u64()).unwrap_or(0); - - let ret = unsafe { - protected_call2( - self.key.slot as u64, - BufferOp::Map as u64, - hint_val, - // self.size, ? - flags.bits() as u64, - ) - }; - - if ret == 0 { - Err(Error::MapFailed) - } else { - Ok(VirtAddr::new(ret)) - } - } - - /// Unmap buffer from current domain's address space. - pub fn unmap(&self) -> Result<(), Error> { - let ret = unsafe { protected_call1(self.key.slot as u64, BufferOp::Unmap as u64, 0) }; - Error::from_code(ret) - } - - /// Query buffer information (no mapping required) - pub fn query(&self) -> Result { - let mut info = BufferInfo::default(); - let ret = unsafe { - protected_call2( - self.key.slot as u64, - BufferOp::Query as u64, - &mut info as *mut _ as u64, - 0, - ) - }; - if ret == 0 { - Ok(info) - } else { - Err(Error::from_code(ret)) - } - } -} - -// Other ops -impl BufferKey

{ - /// Size of this buffer - #[inline] - pub fn size(&self) -> usize { - self.size - } -} - -impl BufferKey { - /// Derive read-only capability (like &T from &mut T) - pub fn derive_readonly(&self, dest_slot: u32) -> Result, Error> { - let ret = unsafe { - protected_call3( - CAPTBL_SELF, - KeyTableOp::CopyDerive, - self.key.slot as u64, - dest_slot as u64, - Rights::READ.bits() as u64, - ) - }; - if ret == 0 { - Ok(BufferKey { - key: Cap::new(dest_slot), - size: self.size, - _perm: PhantomData, - }) - } else { - Err(Error::from_code(ret)) - } - } - - /// Map and get mutable slice - pub fn map_slice_mut(&self) -> Result, Error> { - let addr = self.map(None, MapFlags::NONE)?; - Ok(MappedSliceMut { - cap: self, - ptr: addr.as_mut_ptr(), - len: self.size, - }) - } -} - -impl BufferKey { - /// Map and get immutable slice - pub fn map_slice(&self) -> Result, Error> { - let addr = self.map(None, MapFlags::NONE)?; - Ok(MappedSlice { - cap: self, - ptr: addr.as_ptr(), - len: self.size, - }) - } -} - -#[derive(Default)] -pub struct BufferInfo { - pub size: usize, - pub flags: BufferFlags, - pub is_mapped: bool, - pub mapped_addr: Option, -} - -bitflags::bitflags! { - pub struct MapFlags: u32 { - const NONE = 0; - const FIXED = 1 << 0; // Fail if hint can't be used - const POPULATE = 1 << 1; // Pre-fault all pages - const UNCACHED = 1 << 2; // Override to uncached - } -} - -/// RAII guard that unmaps on drop -pub struct MappedSlice<'a> { - cap: &'a BufferKey, - ptr: *const u8, - len: usize, -} - -impl<'a> MappedSlice<'a> { - pub fn as_slice(&self) -> &[u8] { - unsafe { core::slice::from_raw_parts(self.ptr, self.len) } - } -} - -impl Drop for MappedSlice<'_> { - fn drop(&mut self) { - let _ = self.cap.unmap(); - } -} - -pub struct MappedSliceMut<'a> { - cap: &'a BufferKey, - ptr: *mut u8, - len: usize, -} - -impl<'a> MappedSliceMut<'a> { - pub fn as_slice(&self) -> &[u8] { - unsafe { core::slice::from_raw_parts(self.ptr, self.len) } - } - - pub fn as_mut_slice(&mut self) -> &mut [u8] { - unsafe { core::slice::from_raw_parts_mut(self.ptr, self.len) } - } -} - -impl Drop for MappedSliceMut<'_> { - fn drop(&mut self) { - let _ = self.cap.unmap(); - } -} - -// ============================================== -// == Kernel space object and syscall handling == -// ============================================== - -/// Buffer kernel object - represents a contiguous memory region -pub struct Buffer { - phys_base: PhysAddr, // Physical address (fixed at creation) - size: usize, // Size in bytes (fixed at creation) - flags: BufferFlags, // CACHED, DEVICE, SHARED, etc. - - // Mapping tracking (for single-address-space) - // mappings: SmallVec, // Who has it mapped where -} - -struct Mapping { - domain_id: DomainId, - virt_addr: VirtAddr, - permissions: Rights, // May be less than cap rights (derived cap) -} - -bitflags::bitflags! { - pub struct BufferFlags: u32 { - const CACHED = 1 << 0; // Normal cacheable memory - const DEVICE = 1 << 1; // Device memory (uncached, no speculative) - const SHARED = 1 << 2; // Multi-domain sharing expected - const DMA = 1 << 3; // DMA-capable (physically contiguous) - const EXEC = 1 << 4; // Executable (if supported) - } -} - -/// Buffer operations via protected_call -#[repr(u8)] -enum BufferOp { - Map = 0, // Map into caller's address space - Unmap = 1, // Remove mapping - Query = 2, // Get buffer info (size, flags, mapping status) -} - // ===================== // == Syscall handler == // ===================== +#[inline] pub fn invoke(cap: &KeyEntry, op: u32, arg0: u64, arg1: u64) -> SyscallResult { let buffer = cap.as_buffer()?; let caller = current_domain(); @@ -325,7 +104,3 @@ pub fn invoke(cap: &KeyEntry, op: u32, arg0: u64, arg1: u64) -> SyscallResult { } } } - -impl KernelObject for Buffer { - const TYPE: ObjectType = ObjectType::Buffer; -} diff --git a/kernel/nucleus/src/api/debug_console.rs b/kernel/nucleus/src/api/debug_console.rs index f9e573d35..3e52800a1 100644 --- a/kernel/nucleus/src/api/debug_console.rs +++ b/kernel/nucleus/src/api/debug_console.rs @@ -1,60 +1,16 @@ -use { - crate::api::key::Key, - core::slice, - libsyscall::{SyscallError, SyscallResult, protected_call2}, -}; - -// ================================================== -// == Public user interface, usable from userspace == -// ================================================== -pub struct DebugConsoleKey { - key: Key, -} - -// Root domain gets a DebugConsoleCap, can delegate to others -impl DebugConsoleKey { - pub fn write(&self, s: &str) -> Result<()> { - protected_call2( - self.key.slot, - DebugConsoleOp::Write as u32, - s.as_ptr() as u64, - s.len() as u64, - )?; - Ok(()) - } -} - -// ============================================== -// == Kernel space object and syscall handling == -// ============================================== - -struct DebugConsole; - -impl DebugConsole { - fn handle_write(ptr: u64, len: u64) { - let slice = slice::from_raw_parts(ptr, len as usize); - let buf = [0u8; 4096]; - buf.copy_from_slice(slice); - buf[slice.len()] = 0; - let cstr = unsafe { core::ffi::CStr::from_bytes_with_nul(&buf[..=slice.len() + 1]) }; - libqemu::semihosting::sys_write0_call(cstr); - } -} - -#[repr(u8)] -pub enum DebugConsoleOp { - Write = 0, -} +use libobject::{CapError, Key, debug_console::DebugConsoleOp}; // ===================== // == Syscall handler == // ===================== +#[inline] pub fn invoke(cap: &KeyEntry, op: u32, arg0: u64, arg1: u64) -> SyscallResult { let console = cap.as_debug_console()?; + let op = DebugConsoleOp::try_from(op).map_err(|_| CapError::InvalidOperation)?; + match op { DebugConsoleOp::Write => console.handle_write(arg0, arg1), - _ => Err(SyscallError::InvalidOp), + _ => Err(CapError::InvalidOperation), } - Ok((0, 0)) } diff --git a/kernel/nucleus/src/api/domain.rs b/kernel/nucleus/src/api/domain.rs index b2b48f51e..4620ad848 100644 --- a/kernel/nucleus/src/api/domain.rs +++ b/kernel/nucleus/src/api/domain.rs @@ -1,340 +1,3 @@ -use std::sync::atomic::Ordering; - -// ================================================== -// == Public user interface, usable from userspace == -// ================================================== - -// CSpace layout with self-reference -pub const CAPTBL_SELF: u32 = 0; // Every domain has cap to own captbl here - -/// Domain capability - handle to a protection domain. -/// State queries use shared DCB (no syscall), mutations use CapInvoke. -pub struct DomainCap { - cap: Cap, - id: DomainId, -} - -impl DomainCap { - /// Create a new domain from untyped memory. - /// Convenience wrapper around UntypedRetype. - pub fn create(untyped: &mut UntypedCap, dest_slot: CapSlot) -> Result { - // Domains need ~4KB (12 bits) for kernel structures - untyped_retype( - untyped.split(12)?, // Carve off 4KB - ObjectType::Domain, - 12, - dest_slot, - )?; - - // Domain ID is returned in secondary return value - // (or we query it from the newly created DCB) - Ok(DomainCap { - cap: Cap::new(dest_slot), - id: DomainId(/* ... */), - }) - } - - /// Get domain state from shared DCB - #[inline] - pub fn state(&self) -> DomainState { - let dcb = DCB.get(self.id); - DomainState::try_from(dcb.state.load(Ordering::Acquire)).unwrap_or(DomainState::Inactive) - } - - /// Get time used from shared DCB - #[inline] - pub fn time_used_ns(&self) -> u64 { - let dcb = DCB.get(self.id); - dcb.time_used_ns.load(Ordering::Relaxed) - } - - /// Get pending notifications from shared DCB (NO SYSCALL!) - #[inline] - pub fn pending_notifications(&self) -> u64 { - let dcb = DCB.get(self.id); - dcb.pending_notifications.load(Ordering::Relaxed) - } - - /// Activate domain (make runnable) - requires syscall - pub fn activate(&self) -> Result<(), Error> { - let ret = unsafe { syscall3(self.cap.slot as u64, DomainOp::Activate as u64, 0) }; - Error::from_code(ret) - } - - /// Grant a capability to this domain - requires syscall - pub fn grant(&self, cap: &Cap, dest_slot: CapSlot) -> Result<(), Error> { - let ret = unsafe { - syscall4( - self.cap.slot as u64, - DomainOp::Grant as u64, - cap.slot() as u64, - dest_slot as u64, - ) - }; - Error::from_code(ret) - } - - /// Suspend domain - requires syscall - pub fn suspend(&self) -> Result<(), Error> { - let ret = unsafe { syscall3(self.cap.slot as u64, DomainOp::Suspend as u64, 0) }; - Error::from_code(ret) - } - - /// Resume suspended domain - requires syscall - pub fn resume(&self) -> Result<(), Error> { - let ret = unsafe { syscall3(self.cap.slot as u64, DomainOp::Resume as u64, 0) }; - Error::from_code(ret) - } -} - -// ============================================== -// == Kernel space object and syscall handling == -// ============================================== - -struct Domain; - -impl Domain { - // Initialize new domain's cspace - fn init_cspace(&mut self) { - // Slot 0: capability to this captbl itself - self.cspace[CAPTBL_SELF] = Cap::new(ObjectType::KeyTable, self.cspace_id); - // Now domain can manipulate its own caps - } -} - -#[repr(u8)] -pub enum DomainOp { - Activate = 0, // Make domain runnable - Grant = 1, // Grant capability to domain - Suspend = 2, // Suspend domain - Resume = 3, // Resume suspended domain -} - -#[repr(u32)] -#[derive(Copy, Clone, Debug, PartialEq, Eq)] -pub enum DomainState { - /// Just created, never run - Inactive = 0, - /// Ready to receive CPU time - Runnable = 1, - /// Currently executing (only one domain per CPU) - Running = 2, - /// Waiting on notification/event/endpoint - Blocked = 3, - /// Explicitly suspended by parent - Suspended = 4, - /// Faulted, needs handler - Faulted = 5, -} - -#[repr(u32)] -#[derive(Copy, Clone, Debug)] -pub enum BlockReason { - None = 0, - Notification = 1, // NotifyCap::wait() - EventCount = 2, // EventCountCap::await_ge() - Endpoint = 3, // EndpointCap::call() or recv() - TimeDonated = 4, // Donated time, waiting for return -} - -// pub enum DeactivateReason { -// TimeExhausted, -// BlockedOnEvent(EndpointCap), <-- BlockReason::Endpoint -// Yielded, -// Faulted(fault), -// } - -/// Domain Control Block -/// Our DCB structure - combining Nemesis ideas with our capability model -/// -/// Key insight: split into kernel-private and shared sections -#[repr(C, align(128))] // Cache-line aligned -pub struct DomainControlBlock { - // ═══════════════════════════════════════════════════════════ - // SHARED SECTION (read-only mapped to userspace) - // ═══════════════════════════════════════════════════════════ - // ─── Identity ─── - pub id: DomainId, - pub name: [u8; 24], - - // ─── Execution State (Acquire/Release on state field) ─── - pub state: AtomicU32, // DomainState - pub block_reason: AtomicU32, // BlockReason (if blocked) - pub blocked_on: AtomicU32, // Cap slot we're blocked on - - // ─── Time Accounting (QoS) ─── - /// Cumulative CPU time consumed (nanoseconds) - pub time_used_ns: AtomicU64, - /// Time remaining in current activation - pub time_remaining_ns: AtomicU64, - /// Number of times activated - pub activation_count: AtomicU64, - - // ─── Event State ─── - pub pending_notifications: AtomicU64, // Bitmap of pending notify caps - /// Number of pending events (sum across all endpoints) - pub pending_events: AtomicU32, // Number of event counts with data - - /// Endpoint that caused last wakeup - pub last_event_ep: AtomicU32, - - // ─── Scheduling Parameters ─── - /// Parent scheduler domain - pub scheduler: DomainId, - /// Scheduling priority/parameters - pub priority: u32, - /// Allocation period (for periodic domains) - pub period_ns: u64, - /// CPU allocation per period - pub budget_ns: u64, // slice_ns - - /// Scheduled deadline (absolute time) - pub deadline: AtomicU64, - - // ─── Fault Information ─── - /// Last fault type (if any) - pub fault_type: AtomicU32, - - /// Fault address - pub fault_addr: AtomicU64, - - pub fault_cap: AtomicU32, // Cap slot that caused fault - - // Padding to cache line - _pad: [u8; 16], - // ═══════════════════════════════════════════════════════════ - // PRIVATE SECTION (kernel only, NOT mapped to userspace) - // ═══════════════════════════════════════════════════════════ - // - // This would be in a separate structure or after a page boundary - // - Saved register context - // - Capability space root - // - Kernel stack pointer - // - Etc. -} - -// Verify size for cache alignment -const _: () = assert!(core::mem::size_of::() == 128); - -// 32 DCBs per 4KB page -// Multiple pages for more domains - -// Userspace sees: const DCB_BASE: *const DomainControlBlock = 0xFFFF_0000_0000_0000; // FIXME: pervasives -// Access DCB n: &*DCB_BASE.add(n) - -/// Userspace view of DCB array -/// Mapped read-only at a well-known address -pub struct DcbView { - base: *const DomainControlBlock, -} - -impl DcbView { - /// Get from well-known address (set up by kernel at domain creation) - pub const fn new() -> Self { - Self { - base: 0xFFFF_0000_0000_0000 as *const DomainControlBlock, - } - } - - /// Read any domain's state - #[inline(always)] - pub fn get(&self, id: DomainId) -> &DomainControlBlock { - unsafe { &*self.base.add(id.0 as usize) } - } - - /// Get my own DCB - #[inline(always)] - pub fn myself(&self) -> &DomainControlBlock { - // Current domain ID stored in thread-local or well-known register - self.get(current_domain_id()) - } -} - -// Domain scheduling support in kernel: -impl Nucleus { - /// Called when domain is activated (receives CPU time) - fn activate_domain(&mut self, id: DomainId, time_budget: u64) { - let dcb = self.dcb_mut(id); - - dcb.state - .store(DomainState::Running as u32, Ordering::Release); - dcb.time_remaining_ns.store(time_budget, Ordering::Release); - dcb.deadline.store(now() + time_budget, Ordering::Release); - } - - /// Called on every context switch FROM this domain - fn deactivate_domain(&mut self, id: DomainId, reason: DeactivateReason) { - let dcb = self.dcb_mut(id); - let elapsed = /* calculate from timer */0; - - // Update time accounting - dcb.time_used_ns.fetch_add(elapsed, Ordering::Relaxed); - dcb.time_remaining_ns.fetch_sub(elapsed, Ordering::Relaxed); - - // Update state - match reason { - DeactivateReason::TimeExhausted => { - dcb.state - .store(DomainState::Runnable as u32, Ordering::Release); - } - DeactivateReason::BlockedOnEvent(ep) => { - dcb.state - .store(DomainState::Blocked as u32, Ordering::Release); - dcb.block_reason - .store(BlockReason::Event as u32, Ordering::Release); - dcb.last_event_ep.store(ep, Ordering::Release); - } - DeactivateReason::Yielded => { - dcb.state - .store(DomainState::Runnable as u32, Ordering::Release); - } - DeactivateReason::Faulted(fault) => { - dcb.state - .store(DomainState::Faulted as u32, Ordering::Release); - dcb.fault_type.store(fault.type_code(), Ordering::Release); - dcb.fault_addr.store(fault.addr(), Ordering::Release); - } - } - } - - /// Called when event arrives for blocked domain - fn signal_domain(&mut self, id: DomainId) { - let dcb = self.dcb_mut(id); - - dcb.pending_events.fetch_add(1, Ordering::Release); - - // If blocked on events, make runnable - if dcb.state.load(Ordering::Acquire) == DomainState::Blocked as u32 { - dcb.state - .store(DomainState::Runnable as u32, Ordering::Release); - } - } -} - -// ## Memory Ordering Considerations - -// KERNEL (writer) USERSPACE (reader) -// ─────────────── ────────────────── - -// // Update multiple fields -// dcb.time_used.store(x, Relaxed); -// dcb.time_remaining.store(y, Relaxed); -// dcb.state.store(z, Release); ──────────────────────┐ -// │ │ -// │ Release ensures all │ -// │ prior writes visible │ -// ▼ ▼ -// let state = dcb.state.load(Acquire); -// // Acquire ensures we see -// // all writes before the Release -// let used = dcb.time_used.load(Relaxed); -// let rem = dcb.time_remaining.load(Relaxed); - -// Protocol: -// - Kernel does Release store on state LAST -// - Userspace does Acquire load on state FIRST -// - Then can safely read other fields with Relaxed - // ===================== // == Syscall handler == // ===================== @@ -357,7 +20,3 @@ pub fn invoke(cap: &Cap, op: u32, arg0: u64, arg1: u64) -> SyscallResult { _ => Err(SyscallError::InvalidOp), } } - -impl KernelObject for Domain { - const TYPE: ObjectType = ObjectType::Domain; -} diff --git a/kernel/nucleus/src/api/endpoint.rs b/kernel/nucleus/src/api/endpoint.rs index 192a79f7b..631abf508 100644 --- a/kernel/nucleus/src/api/endpoint.rs +++ b/kernel/nucleus/src/api/endpoint.rs @@ -1,228 +1,3 @@ -// Endpoint capability operations -// -// Endpoints enable synchronous call/return IPC with direct domain switch. -// Unlike Notifications (fire-and-forget) or EventCounts (streaming), -// Endpoints are for request/response patterns. - -use crate::{key::Cap, syscall::protected_call4}; - -// ================================================== -// == Public user interface, usable from userspace == -// ================================================== - -/// Endpoint capability for synchronous IPC -/// -/// Two "views" of the same endpoint: -/// - Client holds EndpointCap (can Call/Send) -/// - Server holds EndpointCap with Recv rights (can Recv/Reply) -/// -/// Badges identify which client is calling (granted during cap derivation) -/// -/// With explicit Reply objects: -/// - recv() returns (badge, msg, ReplyCap) -/// - reply is done via ReplyCap::send(), not endpoint method -/// - reply_recv() takes a ReplyCap to consume -pub struct EndpointCap { - cap: Cap, -} - -impl EndpointCap { - // ═══════════════════════════════════════════════════════════════════ - // CLIENT OPERATIONS - // ═══════════════════════════════════════════════════════════════════ - - /// Call: send message and block waiting for reply - /// - /// This is the primary client→server operation. - /// Performs direct domain switch to receiver (fast path). - /// - /// Returns the reply message (label + data + optional cap) - pub fn call(&self, msg: &Message) -> Result { - let (ret0, ret1, ret2, ret3, ret4, ret5, ret6) = unsafe { - syscall_ipc( - self.cap.slot as u64, - EndpointOp::Call as u64, - msg.label, - msg.data[0], - msg.data[1], - msg.data[2], - msg.data[3], - msg.data[4], - msg.cap.map(|s| s as u64).unwrap_or(u64::MAX), - ) - }; - - if ret0 == 0 { - Ok(Message { - label: ret1, - data: [ret2, ret3, ret4, ret5, 0], - cap: if ret6 != u64::MAX { - Some(ret6 as CapSlot) - } else { - None - }, - }) - } else { - Err(IpcError::from_code(ret0)) - } - } - - /// Send: non-blocking send (no reply expected) - /// - /// If receiver is waiting → message delivered, returns Ok - /// If no receiver → message dropped, returns Err(WouldBlock) - pub fn send(&self, msg: &Message) -> Result<(), IpcError> { - let ret = unsafe { - protected_call4( - self.cap.slot as u64, - EndpointOp::Send as u64, - msg.label, - msg.data[0], - msg.data[1], - msg.data[2], - ) - }; - IpcError::from_code(ret.0) - } - - // ═══════════════════════════════════════════════════════════════════ - // SERVER OPERATIONS - // ═══════════════════════════════════════════════════════════════════ - - /// Recv: block waiting for incoming Call - /// - /// Returns (sender_badge, message, reply_cap) - /// Badge identifies which client called (set during cap derivation) - /// - /// The ReplyCap MUST be used to reply - it's consumed on use. - /// Dropping it without replying will send an error to the caller. - pub fn recv(&self, reply_slot: CapSlot) -> Result<(u64, Message, ReplyCap), IpcError> { - let (ret0, badge, label, d0, d1, d2, d3, d4, cap_slot) = unsafe { - syscall_ipc_recv( - self.cap.slot as u64, - EndpointOp::Recv as u64, - reply_slot as u64, // Where kernel places ReplyCap - ) - }; - - if ret0 == 0 { - let msg = Message { - label, - data: [d0, d1, d2, d3, d4], - cap: if cap_slot != u64::MAX { - Some(cap_slot as CapSlot) - } else { - None - }, - }; - - // Kernel placed ReplyCap in reply_slot - let reply_cap = ReplyCap { - cap: Cap::new(reply_slot), - }; - - Ok((badge, msg, reply_cap)) - } else { - Err(IpcError::from_code(ret0)) - } - } - - /// ReplyRecv: consume reply cap, send reply, AND wait for next message - /// - /// This is the server fast-path: one syscall does Reply + Recv. - /// - /// Takes the ReplyCap to consume (must reply to previous caller) - /// Returns new (badge, msg, reply_cap) for next caller - pub fn reply_recv( - &self, - reply_cap: ReplyCap, - reply_msg: &Message, - next_reply_slot: CapSlot, - ) -> Result<(u64, Message, ReplyCap), IpcError> { - let (ret0, badge, label, d0, d1, d2, d3, d4, cap_slot) = unsafe { - syscall_ipc_reply_recv( - self.cap.slot as u64, - EndpointOp::ReplyRecv as u64, - reply_cap.cap.slot as u64, // Reply cap to consume - next_reply_slot as u64, // Where to place next ReplyCap - reply_msg.label, - reply_msg.data[0], - reply_msg.data[1], - reply_msg.data[2], - reply_msg.data[3], - ) - }; - - // reply_cap consumed by kernel - core::mem::forget(reply_cap); - - if ret0 == 0 { - let msg = Message { - label, - data: [d0, d1, d2, d3, d4], - cap: if cap_slot != u64::MAX { - Some(cap_slot as CapSlot) - } else { - None - }, - }; - - let next_reply = ReplyCap { - cap: Cap::new(next_reply_slot), - }; - - Ok((badge, msg, next_reply)) - } else { - Err(IpcError::from_code(ret0)) - } - } - - /// Forward: pass this call to another endpoint (proxy pattern) - /// - /// Useful for: capability-filtered proxies, load balancers, etc. - /// The forwarded call appears to come from us (our badge) - pub fn forward(&self, target: &EndpointCap, msg: &Message) -> Result<(), IpcError> { - let ret = unsafe { - protected_call4( - self.cap.slot as u64, - EndpointOp::Forward as u64, - target.cap.slot as u64, - msg.label, - msg.data[0], - msg.data[1], - ) - }; - IpcError::from_code(ret.0) - } -} - -impl EndpointCap { - /// Derive a client capability with a specific badge - /// - /// The badge is returned to the server on recv(), identifying the caller. - /// This is how servers distinguish between clients. - pub fn derive_client(&self, badge: u64, dest_slot: CapSlot) -> Result { - let ret = unsafe { - syscall4( - CAPTBL_SELF, // Support deriving directly into a client keytable? - KeyTableOp::CopyDerive, - self.cap.slot as u64, - dest_slot as u64, - Rights::CALL.bits() as u64, // Client can only Call, not Recv - badge, - ) - }; - - if ret == 0 { - Ok(EndpointCap { - cap: Cap::new(dest_slot), - }) - } else { - Err(Error::from_code(ret)) - } - } -} - // Client Domain Server Domain // ───────────── ───────────── // │ │ @@ -258,229 +33,11 @@ impl EndpointCap { // (running) // got reply -// ============================================== -// == Kernel space object and syscall handling == -// ============================================== - -#[repr(u8)] -pub enum EndpointOp { - /// Send message and wait for reply (client operation) - /// Blocks until server receives, processes, and replies - Call = 0, - - /// Send message without waiting (non-blocking) - /// If no receiver waiting, message is dropped (returns error) - Send = 1, - - /// Wait for incoming message (server operation) - /// Blocks until a sender calls - Recv = 2, - - /// Reply to caller and wait for next message (server fast-path) - /// Combines Reply + Recv in single syscall (seL4 optimization) - ReplyRecv = 3, - - /// Reply to caller without waiting for next - Reply = 4, - - /// Forward call to another endpoint (proxy pattern) - /// Transfers the reply capability to the new endpoint - Forward = 5, -} - -/// Message passed through endpoints -/// Fits in registers for fast IPC (~200-500 cycles) -#[repr(C)] -pub struct Message { - /// Message label/opcode - receiver dispatches on this - pub label: u64, - - /// 5 general-purpose data words - pub data: [u64; 5], - - /// Optional capability to transfer (None = no transfer) - /// Sender's cap is moved (not copied) to receiver - pub cap: Option, -} - -impl Message { - pub const fn new(label: u64) -> Self { - Self { - label, - data: [0; 5], - cap: None, - } - } - - pub fn with_data(label: u64, d0: u64, d1: u64, d2: u64, d3: u64, d4: u64) -> Self { - Self { - label, - data: [d0, d1, d2, d3, d4], - cap: None, - } - } - - pub fn with_cap(mut self, cap_slot: CapSlot) -> Self { - self.cap = Some(cap_slot); - self - } -} - -/// Kernel object -struct Endpoint { - /// Domain that can receive on this endpoint (None = anyone) - receiver: Option, - - /// Current state - state: EndpointState, - - /// Waiting senders (if state == Recving, this is empty) - /// Waiting receivers (if state == Sending, this is empty) - wait_queue: WaitQueue, - - /// Message buffer (kernel memory) - msg_regs: [u64; 6], - - /// Badge of sender (filled when message delivered) - sender_badge: u64, - - /// Cap transfer slot - transfer_cap: Option, -} - -enum EndpointState { - Idle, - /// Someone is blocked sending - Sending, - /// Someone is blocked receiving - Recving, -} - -impl Endpoint { - fn handle_call( - &mut self, - caller: &mut Domain, - msg: &[u64; 6], - cap_slot: Option, - badge: u64, - ) -> SyscallResult { - match self.state { - EndpointState::Recving => { - // Receiver waiting! Fast path - direct switch - let receiver = self.wait_queue.pop_front().unwrap(); - - // Copy message to receiver - self.msg_regs = *msg; - self.sender_badge = badge; - self.transfer_cap = cap_slot; - - // Block caller waiting for reply - caller.state = DomainState::Blocked; - caller.block_reason = BlockReason::Endpoint; - - // Wake receiver - receiver.state = DomainState::Runnable; - - // Switch to receiver (donate remaining time) - switch_to(receiver); - - Ok(0) - } - - EndpointState::Idle | EndpointState::Sending => { - // No receiver - block caller - caller.state = DomainState::Blocked; - caller.block_reason = BlockReason::Endpoint; - - self.wait_queue.push_back(caller.id); - self.state = EndpointState::Sending; - - // Store message for when receiver arrives - self.msg_regs = *msg; - self.sender_badge = badge; - self.transfer_cap = cap_slot; - - // Schedule someone else - schedule_next(); - - Ok(0) - } - } - } - - fn handle_recv(&mut self, receiver: &mut Domain, reply_dest_slot: CapSlot) -> SyscallResult { - match self.state { - EndpointState::Sending => { - let sender_info = self.wait_queue.pop_front().unwrap(); - - // Create Reply object for this call - let reply = Reply { - caller: sender_info.domain_id, - state: ReplyState::Pending, - }; - - // Allocate kernel memory for Reply object - FIXME: kernel memory allocation! - // and place capability in receiver's cspace - let reply_cap = kernel_alloc_reply(reply)?; - receiver.cspace.insert(reply_dest_slot, reply_cap)?; - - // Copy message to receiver - receiver.regs.x0 = 0; // Success - receiver.regs.x1 = sender_info.badge; - receiver.regs.x2 = self.msg_regs[0]; // label - receiver.regs.x3 = self.msg_regs[1]; - receiver.regs.x4 = self.msg_regs[2]; - receiver.regs.x5 = self.msg_regs[3]; - receiver.regs.x6 = self.msg_regs[4]; - receiver.regs.x7 = self.msg_regs[5]; - // x8 = transferred cap slot (if any) - - if self.wait_queue.is_empty() { - self.state = EndpointState::Idle; - } - - Ok(0) - } - - EndpointState::Idle | EndpointState::Recving => { - // Block receiver - receiver.state = DomainState::Blocked; - receiver.block_reason = BlockReason::Endpoint; - receiver.blocked_data = reply_dest_slot as u64; // Remember where to put reply cap - - self.wait_queue.push_back(receiver.id); - self.state = EndpointState::Recving; - - schedule_next(); - Ok(0) - } - } - } - - fn handle_reply_recv( - &mut self, - server: &mut Domain, - reply_cap_slot: CapSlot, - next_reply_slot: CapSlot, - reply_msg: &[u64; 6], - ) -> SyscallResult { - // 1. Send reply via the provided reply cap - let reply_cap = server.cspace.take(reply_cap_slot)?; - let reply = reply_cap.as_reply()?; - reply.handle_send(reply_msg, None)?; - - // Reply object is consumed, free kernel memory - kernel_free_reply(reply); - - // 2. Receive next message - self.handle_recv(server, next_reply_slot) - } -} - // ===================== // == Syscall handler == // ===================== +#[inline] pub fn invoke(cap: &Cap, op: u32, arg0: u64, arg1: u64) -> SyscallResult { let ep = cap.as_endpoint()?; match op { @@ -493,7 +50,3 @@ pub fn invoke(cap: &Cap, op: u32, arg0: u64, arg1: u64) -> SyscallResult { _ => Err(SyscallError::InvalidOp), } } - -impl KernelObject for Endpoint { - const TYPE: ObjectType = ObjectType::Endpoint; -} diff --git a/kernel/nucleus/src/api/event_count.rs b/kernel/nucleus/src/api/event_count.rs index 0cb46c963..2a73ba244 100644 --- a/kernel/nucleus/src/api/event_count.rs +++ b/kernel/nucleus/src/api/event_count.rs @@ -1,87 +1,3 @@ -// Reed-Kanodia event counts - -// ================================================== -// == Public user interface, usable from userspace == -// ================================================== - -/// Event count capability - monotonic counter for exact event tracking. -/// Best for: streaming, flow control, producer-consumer coordination. -/// -/// Unlike notifications, every advance() is counted - no coalescing. -/// This lets consumers know exactly how far behind they are. -pub struct EventCountKey { - key: Key, -} - -impl EventCountCap { - /// Advance: atomically increment counter by delta. - /// Returns new value. Never blocks. ~30 cycles. - #[inline] - pub fn advance(&self, delta: u64) -> u64 { - protected_call1(self.key.slot as u64, EventOp::Advance as u64, delta) - } - - /// Await: block until value >= target. - /// Returns current value (may be > target if producer is fast). - #[inline] - pub fn await_ge(&self, target: u64) -> u64 { - protected_call1(self.key.slot as u64, EventOp::Await as u64, target) - } - - /// Read: get current value without blocking. - #[inline] - pub fn read(&self) -> u64 { - protected_call0(self.key.slot as u64, EventOp::Read) - } -} - -/// Helper: tracks consumer position for a single reader -pub struct EventCountReader { - ec: EventCountKey, - last_seen: u64, -} - -impl EventCountReader { - pub fn new(ec: EventCountKey) -> Self { - let initial = ec.read(); - Self { - ec, - last_seen: initial, - } - } - - /// Wait for next event(s), returns count since last wait - pub fn wait_next(&mut self) -> u64 { - let target = self.last_seen + 1; - let current = self.ec.await_ge(target); - let delta = current - self.last_seen; - self.last_seen = current; - delta - } - - /// Check how many events pending without blocking - pub fn pending(&self) -> u64 { - self.ec.read() - self.last_seen - } -} - -// ============================================== -// == Kernel space object and syscall handling == -// ============================================== - -// Kernel object -struct EventCount { - value: u64, // Monotonically increasing counter - waiters: WaitQueue, // Domains waiting for value >= target -} - -#[repr(u8)] -enum EventCountOp { - Advance = 0, - Await = 1, - Read = 2, -} - // ===================== // == Syscall handler == // ===================== @@ -101,7 +17,3 @@ pub fn invoke(cap: &Cap, op: u32, arg0: u64) -> SyscallResult { _ => Err(SyscallError::InvalidOp), } } - -impl KernelObject for EventCount { - const TYPE: ObjectType = ObjectType::EventCount; -} diff --git a/kernel/nucleus/src/api/key_entry.rs b/kernel/nucleus/src/api/key_entry.rs index 113cd8874..2704f3a77 100644 --- a/kernel/nucleus/src/api/key_entry.rs +++ b/kernel/nucleus/src/api/key_entry.rs @@ -2,6 +2,11 @@ // KEY ENTRY (CAPABILITY TABLE ENTRY) // ═══════════════════════════════════════════════════════════════════ +use crate::{ + libobject::{KeySlot, ObjectType}, + objects::{NucleusObject, ObjectRef}, +}; + /// A single entry in a domain's capability table (KeyTable). /// /// Size: 32 bytes (fits nicely in cache) @@ -36,6 +41,7 @@ pub struct KeyEntry { /// Generation counter (detect stale capabilities) generation: u32, } +// FIXME: ^ for Untyped this should be the object itself... impl KeyEntry { /// Create a null/empty entry @@ -53,7 +59,7 @@ impl KeyEntry { } /// Create a new capability entry - pub fn new( + pub fn new( object: &T, rights: Rights, badge: u32, @@ -94,13 +100,13 @@ impl KeyEntry { /// Access the underlying object with type checking #[inline] - pub fn as_object(&self) -> Result<&T, CapError> { + pub fn as_object(&self) -> Result<&T, CapError> { self.object_ref.as_type() } /// Access the underlying object mutably with type checking #[inline] - pub fn as_object_mut(&mut self) -> Result<&mut T, CapError> { + pub fn as_object_mut(&mut self) -> Result<&mut T, CapError> { self.object_ref.as_type_mut() } } diff --git a/kernel/nucleus/src/api/key_table.rs b/kernel/nucleus/src/api/key_table.rs index 47211f493..ab1e7c077 100644 --- a/kernel/nucleus/src/api/key_table.rs +++ b/kernel/nucleus/src/api/key_table.rs @@ -1,125 +1,4 @@ -use crate::{ - SyscallError, - api::{protected_call1, protected_call4}, -}; - -// ================================================== -// == Public user interface, usable from userspace == -// ================================================== - -pub struct KeyTableKey { - key: Key, -} - -// Userspace KeyManager must track parent→child relationships, -// kernel only manages flat key tables. - -impl KeyTableKey { - // This naturally supports cross-domain derivation: - // "Create a read-only view of my buffer in their cspace" - // derive(&my_captbl, buffer_slot, &their_captbl, their_slot, Rights::READ)?; - /// Copy with derivation in single syscall - pub fn copy_derive( - &self, - src_slot: u32, - dst_captbl: &KeyTableKey, // Could be same or different! - dst_slot: u32, - rights: Rights, - ) -> Result<()> { - protected_call4( - self.key.slot(), - KeyTableOp::CopyDerive, - src_slot, - dst_captbl.slot(), - dst_slot, - rights.bits(), - ) - } - - // fn activate(&self, slot: u32, object: KernelObject) -> Result<()> { - // let captbl = self.get_captbl_mut()?; - // // SAFETY: User specifies slot, but kernel validates - // if slot >= captbl.len() { - // return Err(Error::SlotOutOfRange); - // } - // if captbl.slots[slot].is_valid() { - // return Err(Error::SlotOccupied); // User's bookkeeping was wrong - // } - // // Kernel creates the cap - user never touches this - // captbl.slots[slot] = Cap::new(object); - // Ok(()) - // } - - fn r#move() {} - - fn delete(&mut self, slot: u32) -> Result<()> { - // TODO: Must invoke on self-captbl cap - protected_call1(self.key.slot(), KeyTableOp::Delete, slot) - } - - // Revoke all children of cap in slot - fn revoke(&self, captbl: &CaptblCap, slot: u32) -> Result<()> { - protected_call1(self.key.slot(), KeyTableOp::Revoke, slot) - } - - // User code to copy cap to another domain (if you have their captbl cap): - fn grant_to(my_slot: u32, their_captbl: &CaptblCap, their_slot: u32) -> Result<()> { - protected_call3( - self.key.slot(), - KeyTableOp::CopyDerive, - my_slot, - their_captbl.slot(), - their_slot, - same_rights, - ) - } -} - -// ============================================== -// == Kernel space object and syscall handling == -// ============================================== - -#[repr(u8)] -enum KeyTableOp { - CopyDerive = 0, // copy cap between slots or create derived cap with reduced rights - Move = 1, // move cap between slots - Delete = 2, // delete cap at slot - Revoke = 4, // revoke all children of cap -} - -impl KeyTableOp { - fn try_from(op: u32) -> Result { - match op { - 0 => Ok(KeyTableOp::CopyDerive), - 1 => Ok(KeyTableOp::Move), - 2 => Ok(KeyTableOp::Delete), - 3 => Ok(KeyTableOp::Revoke), - _ => Err(SyscallError::InvalidOp), - } - } -} - -struct KeyTable { - slots: [u64; 256], - len: usize, -} - -impl KeyTable { - fn delete(&self, slot: u32) -> Result<()> { - if slot >= self.len() { - return Err(Error::SlotOutOfRange); - } - - if !self.slots[slot].is_valid() { - return Err(Error::SlotEmpty); - } - - let cap = self.slots[slot].take(); - cap.destroy()?; - - Ok(()) - } -} +use libobject::key_table::KeyTableOp; // ===================== // == Syscall handler == @@ -145,122 +24,3 @@ pub fn invoke(key: &Key, op: u32, args: &[u64]) -> SyscallResult { } } } - -////========== -////========== -////========== -////========== -////========== - -// ═══════════════════════════════════════════════════════════════════ -// KEY TABLE (CAPABILITY TABLE / CNODE) -// ═══════════════════════════════════════════════════════════════════ - -/// Slot index in a KeyTable -#[derive(Copy, Clone, Debug, PartialEq, Eq)] -#[repr(transparent)] -pub struct KeySlot(pub u16); - -impl KeySlot { - pub const NULL: KeySlot = KeySlot(0); - pub const SELF_DOMAIN: KeySlot = KeySlot(1); - pub const PARENT_DOMAIN: KeySlot = KeySlot(2); - // ... other well-known slots -} - -/// A capability table for a domain. -/// -/// This is what seL4 calls a CNode. Each domain has one. -/// The table itself is a kernel object that can be referenced -/// by capabilities (for capability space manipulation). -pub struct KeyTable { - /// The actual capability entries - entries: [KeyEntry; Self::NUM_SLOTS], - /// Domain that owns this table - owner: DomainId, - /// Number of valid entries (for iteration) - count: u16, -} - -impl KeyTable { - /// Number of slots per table (power of 2 for fast indexing) - pub const NUM_SLOTS: usize = 256; - - /// Create a new empty capability table - pub fn new(owner: DomainId) -> Self { - Self { - entries: [const { KeyEntry::null() }; Self::NUM_SLOTS], - owner, - count: 0, - } - } - - /// Lookup a capability by slot index - #[inline] - pub fn lookup(&self, slot: KeySlot) -> Result<&KeyEntry, CapError> { - let idx = slot.0 as usize; - if idx >= Self::NUM_SLOTS { - return Err(CapError::InvalidSlot(slot)); - } - - let entry = &self.entries[idx]; - if !entry.is_valid() { - return Err(CapError::EmptySlot(slot)); - } - - Ok(entry) - } - - /// Lookup a capability mutably - #[inline] - pub fn lookup_mut(&mut self, slot: KeySlot) -> Result<&mut KeyEntry, CapError> { - let idx = slot.0 as usize; - if idx >= Self::NUM_SLOTS { - return Err(CapError::InvalidSlot(slot)); - } - - let entry = &mut self.entries[idx]; - if !entry.is_valid() { - return Err(CapError::EmptySlot(slot)); - } - - Ok(entry) - } - - /// Insert a capability at a specific slot - pub fn insert(&mut self, slot: KeySlot, entry: KeyEntry) -> Result<(), CapError> { - let idx = slot.0 as usize; - if idx >= Self::NUM_SLOTS { - return Err(CapError::InvalidSlot(slot)); - } - - if self.entries[idx].is_valid() { - return Err(CapError::SlotOccupied(slot)); - } - - self.entries[idx] = entry; - self.count += 1; - Ok(()) - } - - /// Remove a capability from a slot - pub fn remove(&mut self, slot: KeySlot) -> Result { - let idx = slot.0 as usize; - if idx >= Self::NUM_SLOTS { - return Err(CapError::InvalidSlot(slot)); - } - - let entry = core::mem::replace(&mut self.entries[idx], KeyEntry::null()); - if entry.is_valid() { - self.count -= 1; - Ok(entry) - } else { - Err(CapError::EmptySlot(slot)) - } - } -} - -// KeyTable is itself a kernel object -impl KernelObject for KeyTable { - const TYPE: ObjectType = ObjectType::KeyTable; -} diff --git a/kernel/nucleus/src/api/mod.rs b/kernel/nucleus/src/api/mod.rs index 2dc4c4f5c..6f6b3b2d6 100644 --- a/kernel/nucleus/src/api/mod.rs +++ b/kernel/nucleus/src/api/mod.rs @@ -1,5 +1,169 @@ -mod arch; -mod debug_console; -mod key; -mod nucleus_object; -mod object_type; +use { + crate::objects::DebugConsole, + libobject::{ArchType, CapError, CoreType, KeySlot, ObjectType}, +}; + +// pub mod arch; +pub mod debug_console; +pub mod key_entry; +// pub mod key_table; + +// ═════════════════ +// SYSCALL DISPATCH +// ═════════════════ + +/// Main capability invocation handler with two-level dispatch. +/// +/// First: single bit test to separate arch vs core +/// Then: smaller match within each category +/// +/// This is more branch-predictor friendly because: +/// 1. The arch bit test is highly predictable (most calls are core) +/// 2. Each sub-match has fewer cases +#[inline] +pub fn handle_cap_invoke( + nucleus: &mut Nucleus, + cap_slot: u32, + op: u32, + args: &[u64; 6], +) -> Result<(u64, u64), CapError> { + let domain = nucleus.current_domain_mut()?; + let slot = KeySlot(cap_slot); + let entry = domain.keytable.lookup_mut(slot)?; + let obj_type = entry.object_type(); + + if obj_type.is_arch() { + // Architecture-specific dispatch (less common path) + arch_invoke::(nucleus, entry, obj_type, op, args) + } else { + // Core dispatch (common path) + core_invoke::(nucleus, entry, obj_type, op, args) + } +} + +/// Core object dispatch +#[inline(always)] +fn core_invoke( + nucleus: &mut Nucleus, + entry: &mut KeyEntry, + obj_type: ObjectType, + op: u32, + args: &[u64; 6], +) -> Result<(u64, u64), CapError> { + let core_type = CoreType::try_from(obj_type)?; + + match core_type { + CoreType::Null => Err(CapError::NullCapability), + + // CoreType::Untyped => { + // let untyped = entry.as_object_mut::()?; + // // Untyped::invoke(untyped, ....) + // api::untyped::invoke(untyped, entry.rights(), op, args, &mut nucleus.pools) + // } + CoreType::DebugConsole => { + let debug_console = entry.as_object_mut::()?; + DebugConsole::invoke(debug_console, parampampam) + } // CoreType::Domain => { + // let domain = entry.as_object_mut::()?; + // api::domain::invoke(domain, entry.rights(), op, args) + // } + + // CoreType::KeyTable => { + // let kt = entry.as_object_mut::()?; + // api::keytable::invoke(kt, entry.rights(), op, args) + // } + + // CoreType::Notification => { + // let notify = entry.as_object_mut::()?; + // api::notification::invoke(notify, entry.rights(), entry.badge(), op, args) + // } + + // CoreType::EventCount => { + // let ec = entry.as_object_mut::()?; + // api::event_count::invoke(ec, entry.rights(), op, args) + // } + + // CoreType::Endpoint => { + // let ep = entry.as_object_mut::()?; + // api::endpoint::invoke(ep, entry.rights(), entry.badge(), op, args, nucleus) + // } + + // CoreType::Time => { + // let time = entry.as_object_mut::()?; + // api::time::invoke(time, entry.rights(), op, args, nucleus) + // } + + // CoreType::Buffer => { + // let buf = entry.as_object_mut::()?; + // api::buffer::invoke(buf, entry.rights(), op, args) + // } + + // CoreType::Reply => { + // let reply = entry.as_object_mut::()?; + // api::reply::invoke(reply, op, args, nucleus) + // } + _ => Err(CapError::UnsupportedCoreType), + } +} + +/// Architecture-specific dispatch - defined per architecture +#[inline(always)] +fn arch_invoke( + nucleus: &mut Nucleus, + entry: &mut KeyEntry, + obj_type: ObjectType, + op: u32, + args: &[u64; 6], +) -> Result<(u64, u64), CapError> { + let arch_type = ArchType::try_from(obj_type)?; + + match arch_type { + // ArchType::Frame => { + // let frame = entry.as_object_mut::()?; + // A::invoke_frame(frame, entry.rights(), op, args, nucleus) // or A::Frame::invoke()? + // } + + // ArchType::PageTable => { + // let pt = entry.as_object_mut::()?; + // A::invoke_page_table(pt, entry.rights(), op, args, nucleus) + // } + + // ArchType::VSpace => { + // let vspace = entry.as_object_mut::()?; + // A::invoke_vspace(vspace, entry.rights(), op, args, nucleus) + // } + + // ArchType::ASIDPool => { + // let pool = entry.as_object_mut::()?; + // A::invoke_asid_pool(pool, entry.rights(), op, args, nucleus) + // } + + // ArchType::ASID => { + // let asid = entry.as_object_mut::()?; + // A::invoke_asid(asid, entry.rights(), op, args) + // } + + // ArchType::IOSpace => { + // // May not be supported on all architectures + // A::invoke_io_space(entry, op, args, nucleus) + // } + + // ArchType::IOPort => { + // // x86 only + // #[cfg(target_arch = "x86_64")] + // { + // let port = entry.as_object_mut::()?; + // x86_64::invoke_io_port(port, entry.rights(), op, args) + // } + // #[cfg(not(target_arch = "x86_64"))] + // { + // Err(CapError::UnsupportedArchType(arch_type)) + // } + // } + + // ArchType::IRQHandler => A::invoke_irq_handler(entry, op, args, nucleus), + + // ArchType::IRQControl => A::invoke_irq_control(entry, op, args, nucleus), + x => Err(CapError::UnsupportedArchType(x)), + } +} diff --git a/kernel/nucleus/src/api/notification.rs b/kernel/nucleus/src/api/notification.rs index 16c472b07..2ed860a5b 100644 --- a/kernel/nucleus/src/api/notification.rs +++ b/kernel/nucleus/src/api/notification.rs @@ -1,131 +1,46 @@ -use crate::syscall::{protected_call0, protected_call1}; - -// ================================================== -// == Public user interface, usable from userspace == -// ================================================== - -/// Notification capability - bitmap-based async signaling. -/// Best for: IRQs, completion events, wakeups. -pub struct NotificationKey { - key: Key, -} - -impl NotificationKey { - /// Signal: atomic OR into bitmap (always non-blocking) - /// Multiple signals to same bit coalesce. - /// ~30 cycles, no domain switch needed - #[inline] - pub fn signal(&self, bits: u64) -> Result<()> { - protected_call1(self.slot, NotifyOp::Signal as u32, bits)?; - Ok(()) - } - - /// Wait: block until any bit set, returns + clears ALL bits - #[inline] - pub fn wait(&self) -> Result { - protected_call0(self.slot, NotifyOp::Wait as u32) - } - - /// Poll: non-blocking check, returns + clears bits - #[inline] - pub fn poll(&self) -> u64 { - protected_call0(self.cap.slot as u64, NotifyOp::Poll as u32) - } -} - -// ============================================== -// == Kernel space object and syscall handling == -// ============================================== - -struct Notification { - state: u64, // Bitmap - waiters: WaitQueue, // Blocked domains - bound: Option, // Optional bound domain for fast wakeup -} - -#[repr(u8)] -enum NotifyOp { - Signal = 0, - Wait = 1, - Poll = 2, -} - -impl Notification { - fn signal(&mut self, bits: u64) {} - fn wait() { - // if bits are already set, clear and immediately return - // otherwise block the domain.. - } - fn poll() {} -} - // ===================== // == Syscall handler == // ===================== -pub fn invoke(cap: &Cap, op: u32, arg0: u64) -> SyscallResult { - let notify = cap.as_notification()?; - match op { - NotifyOp::Signal => { - notify.signal(arg0); // atomic OR - Ok(0) - } - NotifyOp::Wait => { - Ok(notify.wait()) // blocks, returns + clears - } - NotifyOp::Poll => { - Ok(notify.poll()) // non-blocking - } - _ => Err(SyscallError::InvalidOp), - } -} - pub fn invoke( - notify: &mut Notification, - rights: Rights, - badge: u32, - op: u32, - args: &[u64; 6], - ) -> Result<(u64, u64), CapError> { - let op = NotifyOp::try_from(op as u8) - .map_err(|_| CapError::InvalidOperation)?; + notify: &mut Notification, + rights: Rights, + badge: u32, + op: u32, + args: &[u64; 6], +) -> Result<(u64, u64), CapError> { + let op = NotifyOp::try_from(op as u8).map_err(|_| CapError::InvalidOperation)?; - match op { - NotifyOp::Signal => { - // Check we have send rights - if !rights.contains(Rights::SEND) { - return Err(CapError::InsufficientRights); - } - - // Signal using badge (or args[0] if badge is 0) - let bits = if badge != 0 { badge as u64 } else { args[0] }; - notify.signal(bits); - Ok((0, 0)) - } + match op { + NotifyOp::Signal => { + // Check we have send rights + if !rights.contains(Rights::SEND) { + return Err(CapError::InsufficientRights); + } - NotifyOp::Wait => { - // Check we have receive rights - if !rights.contains(Rights::RECV) { - return Err(CapError::InsufficientRights); - } + // Signal using badge (or args[0] if badge is 0) + let bits = if badge != 0 { badge as u64 } else { args[0] }; + notify.signal(bits); + Ok((0, 0)) + } - let bits = notify.wait(current_domain_mut()); - Ok((bits, 0)) - } + NotifyOp::Wait => { + // Check we have receive rights + if !rights.contains(Rights::RECV) { + return Err(CapError::InsufficientRights); + } - NotifyOp::Poll => { - if !rights.contains(Rights::RECV) { - return Err(CapError::InsufficientRights); - } + let bits = notify.wait(current_domain_mut()); + Ok((bits, 0)) + } - let bits = notify.poll(); - Ok((bits, 0)) - } + NotifyOp::Poll => { + if !rights.contains(Rights::RECV) { + return Err(CapError::InsufficientRights); } + + let bits = notify.poll(); + Ok((bits, 0)) } } } - -impl KernelObject for Notification { - const TYPE: ObjectType = ObjectType::Notification; -} diff --git a/kernel/nucleus/src/api/reply.rs b/kernel/nucleus/src/api/reply.rs index ba26fadf2..433420d69 100644 --- a/kernel/nucleus/src/api/reply.rs +++ b/kernel/nucleus/src/api/reply.rs @@ -1,164 +1,3 @@ -//! Endpoint IPC Reply object - -// ================================================== -// == Public user interface, usable from userspace == -// ================================================== - -/// Reply capability - one-shot reply to a blocked caller -/// -/// Created by kernel when a Call arrives, consumed when reply is sent. -/// This is a LINEAR type - must be used exactly once (or explicitly dropped). -/// -/// Key insight from seL4 MCS: making reply explicit enables: -/// - Async reply (store reply cap, reply later) -/// - Delegation (pass reply cap to helper domain) -/// - Multiple outstanding calls (each has own reply cap) -pub struct ReplyKey { - key: Key, -} - -impl ReplyKey { - /// Send reply to the blocked caller - /// - /// Consumes self - reply cap is one-shot! - /// After this, the caller is unblocked with the response. - pub fn send(self, msg: &Message) -> Result<(), IpcError> { - let ret = unsafe { - protected_call4( - self.key.slot as u64, - ReplyOp::Send as u64, - msg.label, - msg.data[0], - msg.data[1], - msg.data[2], - ) - }; - - // Consumed - don't run Drop (kernel already invalidated) - core::mem::forget(self); - - IpcError::from_code(ret.0) - } - - /// Send reply with capability transfer - pub fn send_with_key(self, msg: &Message, key: KeySlot) -> Result<(), IpcError> { - let ret = unsafe { - syscall_ipc_reply( - self.cap.slot as u64, - ReplyOp::SendWithCap as u64, - msg.label, - msg.data[0], - msg.data[1], - msg.data[2], - msg.data[3], - msg.data[4], - cap as u64, - ) - }; - - // Consumed - don't run Drop (kernel already invalidated) - core::mem::forget(self); - - IpcError::from_code(ret.0) - } -} - -/// Dropping a ReplyKey without sending is an ERROR for the caller. -/// The caller remains blocked forever (or until timeout/cancellation). -/// -/// In debug builds, we panic. In release, we send an error reply. -impl Drop for ReplyKey { - fn drop(&mut self) { - // Reply cap dropped without sending - this is usually a bug! - // Send error reply to unblock caller - #[cfg(debug_assertions)] - panic!("ReplyKey dropped without sending reply!"); - - #[cfg(not(debug_assertions))] - unsafe { - protected_call1( - self.cap.slot as u64, - ReplyOp::SendError as u64, - IpcError::ReplyDropped as u64, - ); - } - } -} - -// ============================================== -// == Kernel space object and syscall handling == -// ============================================== - -/// Reply kernel object -struct Reply { - /// Domain waiting for this reply - caller: DomainId, - - /// State of this reply object - state: ReplyState, -} - -#[derive(Clone, Copy, PartialEq)] -enum ReplyState { - /// Caller is blocked waiting - Pending, - /// Reply was sent, object is consumed - Used, - /// Caller cancelled or timed out - Cancelled, -} - -#[repr(u8)] -enum ReplyOp { - Send = 0, // Send reply message - SendWithCap = 1, // Send reply with cap transfer - SendError = 2, // Send error (used by Drop) -} - -impl Reply { - fn handle_send(&mut self, msg: &[u64; 6], cap_slot: Option) -> SyscallResult { - match self.state { - ReplyState::Pending => { - let caller = get_domain_mut(self.caller); - - // Copy reply to caller's registers - caller.regs.x0 = 0; // Success - caller.regs.x1 = msg[0]; // label - caller.regs.x2 = msg[1]; - caller.regs.x3 = msg[2]; - caller.regs.x4 = msg[3]; - caller.regs.x5 = msg[4]; - caller.regs.x6 = msg[5]; - - // Transfer cap if present - if let Some(slot) = cap_slot { - let cap = current_domain().cspace.take(slot)?; - caller.cspace.insert(RECEIVED_CAP_SLOT, cap)?; - caller.regs.x7 = RECEIVED_CAP_SLOT as u64; - } else { - caller.regs.x7 = u64::MAX; - } - - // Wake caller - caller.state = DomainState::Runnable; - - // Mark reply object as used (one-shot) - self.state = ReplyState::Used; - - Ok(0) - } - - ReplyState::Used => Err(SyscallError::ReplyAlreadyUsed), - - ReplyState::Cancelled => { - // Caller gave up - just consume the reply cap - self.state = ReplyState::Used; - Ok(0) // Not an error, just no-op - } - } - } -} - // CLIENT DOMAIN KERNEL SERVER DOMAIN // ───────────── ────── ───────────── // @@ -192,7 +31,3 @@ pub fn invoke(cap: &CapEntry, op: u32, arg0: u64, arg1: u64) -> SyscallResult { _ => Err(SyscallError::InvalidOp), } } - -impl KernelObject for Reply { - const TYPE: ObjectType = ObjectType::Reply; -} diff --git a/kernel/nucleus/src/api/time.rs b/kernel/nucleus/src/api/time.rs index 01058975e..3fbae6b1e 100644 --- a/kernel/nucleus/src/api/time.rs +++ b/kernel/nucleus/src/api/time.rs @@ -1,82 +1,3 @@ -// ================================================== -// == Public user interface, usable from userspace == -// ================================================== - -/// Time capability — represents a slice of CPU time -pub struct TimeKey { - key: Key, - ) -> Result<(), CapError> { - // Determine object size based on type - let obj_size = if obj_type.is_core() { - core_object_size(obj_type, size_bits)? - } else { - A::validate_retype(obj_type, size_bits)? - }; - - // Check we have enough memory - if self.watermark + obj_size > self.size { - return Err(CapError::InsufficientMemory); - } - - // Allocate from untyped - let obj_addr = self.phys_addr.offset(self.watermark); - self.watermark += obj_size; - - // Create the object based on type - let entry = if obj_type.is_core() { - self.create_core_object(obj_type, obj_addr, size_bits, pools)? - } else { - self.create_arch_object::(obj_type, obj_addr, size_bits, pools)? - }; - - // Insert capability into destination slot - dest_keytable.insert(dest_slot, entry)?; - - Ok(()) - } - - fn create_core_object( - &mut self, - obj_type: ObjectType, - phys_addr: PhysAddr, - size_bits: u8, - pools: &mut NucleusPools, - ) -> Result { - match obj_type { - ObjectType::Notification => { - let notify = Notification::new(); - let obj = pools - .notifications - .allocate(notify) - .ok_or(CapError::PoolExhausted)?; - Ok(KeyEntry::new(obj, Rights::all(), 0, None)) - } - - ObjectType::EventCount => { - let ec = EventCount::new(); - let obj = pools - .event_counts - .allocate(ec) - .ok_or(CapError::PoolExhausted)?; - Ok(KeyEntry::new(obj, Rights::all(), 0, None)) - } - - ObjectType::Domain => { - let domain = Domain::new(phys_addr); - let obj = pools - .domains - .allocate(domain) - .ok_or(CapError::PoolExhausted)?; - Ok(KeyEntry::new(obj, Rights::all(), 0, None)) - } - - ObjectType::Time => { - let time = TimeSlice::new_default(); - let obj = pools - .time_slices - .allocate(time) - .ok_or(CapError::PoolExhausted)?; - Ok(KeyEntry::new(obj, Rights::all(), 0, None)) - } - - ObjectType::Endpoint => { - let ep = Endpoint::new(); - let obj = pools - .endpoints - .allocate(ep) - .ok_or(CapError::PoolExhausted)?; - Ok(KeyEntry::new(obj, Rights::all(), 0, None)) - } - - ObjectType::Buffer => { - let size = 1usize << size_bits; - let buffer = Buffer::new(phys_addr, size); - let obj = pools - .buffers - .allocate(buffer) - .ok_or(CapError::PoolExhausted)?; - Ok(KeyEntry::new(obj, Rights::READ | Rights::WRITE, 0, None)) - } - - ObjectType::KeyTable => { - let kt = KeyTable::new_empty(); - let obj = pools - .keytables - .allocate(kt) - .ok_or(CapError::PoolExhausted)?; - Ok(KeyEntry::new(obj, Rights::all(), 0, None)) - } - - ObjectType::Untyped => { - // Split: create a child untyped - let child_size = 1usize << size_bits; - let child = Untyped { - phys_addr, - size: child_size, - watermark: 0, - is_device: self.is_device, - }; - let obj = pools - .untypeds - .allocate(child) - .ok_or(CapError::PoolExhausted)?; - Ok(KeyEntry::new(obj, Rights::all(), 0, None)) - } - - ObjectType::Reply => { - let reply = Reply::new(); - let obj = pools - .replies - .allocate(reply) - .ok_or(CapError::PoolExhausted)?; - // Reply caps have restricted rights - Ok(KeyEntry::new(obj, Rights::WRITE, 0, None)) - } - - _ => Err(CapError::InvalidObjectType), - } - } - - fn create_arch_object( - &mut self, - obj_type: ObjectType, - phys_addr: PhysAddr, - size_bits: u8, - pools: &mut NucleusPools, - ) -> Result { - let obj_ref = A::create_arch_object(obj_type, phys_addr, size_bits, &mut pools.arch)?; - - // Default rights based on type - let rights = match obj_type { - ObjectType::Frame => Rights::READ | Rights::WRITE, - ObjectType::PageTable => Rights::all(), - ObjectType::VSpace => Rights::all(), - ObjectType::ASIDPool => Rights::all(), - ObjectType::ASID => Rights::all(), - _ => Rights::all(), - }; - - Ok(KeyEntry::from_ref(obj_ref, rights, 0, None)) - } -} - -fn core_object_size(obj_type: ObjectType, size_bits: u8) -> Result { - match obj_type { - ObjectType::Notification => Ok(core::mem::size_of::()), - ObjectType::EventCount => Ok(core::mem::size_of::()), - ObjectType::Time => Ok(core::mem::size_of::()), - ObjectType::Endpoint => Ok(core::mem::size_of::()), - ObjectType::Reply => Ok(core::mem::size_of::()), - ObjectType::Domain => Ok(4096), - ObjectType::KeyTable => Ok(core::mem::size_of::()), - ObjectType::Buffer | ObjectType::Untyped => { - if size_bits > 30 { - Err(CapError::InvalidSize) - } else { - Ok(1usize << size_bits) - } - } - _ => Err(CapError::InvalidObjectType), - } -} - -impl NucleusObject for Untyped { - const TYPE: ObjectType = ObjectType::Untyped; -} diff --git a/kernel/nucleus/src/main.rs b/kernel/nucleus/src/main.rs index 84b10fa29..e21c1146b 100644 --- a/kernel/nucleus/src/main.rs +++ b/kernel/nucleus/src/main.rs @@ -23,12 +23,36 @@ #![feature(core_intrinsics)] use { - cfg_if::cfg_if, core::{arch::asm, cell::UnsafeCell, panic::PanicInfo, time::Duration}, libcpu::endless_sleep, liblocking::IRQSafeNullLock, liblog::{info, println, warn}, libmemory::mmu::AccessPermissions, libqemu::semi_println + crate::{ + api::{key_table::KeySlot, object_type::ArchType}, + nucleus::Nucleus, + }, + cfg_if::cfg_if, + core::{ + arch::asm, + cell::{LazyCell, UnsafeCell}, + panic::PanicInfo, + time::Duration, + }, + libcpu::endless_sleep, + liblocking::IRQSafeNullLock, + liblog::{info, println, warn}, + libmemory::mmu::AccessPermissions, + libqemu::semi_println, + libsyscall::CapError, }; +/// Syscall API - capability invocation handlers mod api; +/// Nucleus objects implementations +mod objects; +/// Exception vectors triggering syscall handing and general IRQ routing mod vectors; +/// Global kernel state, protected by The Great Kernel Lock +static mut NUCLEUS: IRQSafeNullLock>> = + IRQSafeNullLock::new(LazyCell::new(|| Nucleus:: {})); + #[panic_handler] fn panicked(info: &PanicInfo) -> ! { libmachine::panic::handler(info) @@ -107,7 +131,7 @@ fn cap_invoke_handler( get_pc() ); - handle_cap_invoke(NUCLEUS.lock(), cap_slot, op, args) + api::handle_cap_invoke(NUCLEUS.lock(), cap_slot, op, args) // let cap = current_domain().keytable.lookup(cap_slot)?; // let args = &[arg0, arg1, arg2, arg3, arg4, arg5]; // FIXME temp @@ -130,167 +154,6 @@ fn cap_invoke_handler( // } } -/// Global kernel state, protected by The Great Kernel Lock -static mut NUCLEUS: IRQSafeNullLock>> = IRQSafeNullLock::new(LazyLock::new(|| Nucleus)); - -// ═══════════════════════════════════════════════════════════════════ -// SYSCALL DISPATCH WITH ARCH OBJECTS -// ═══════════════════════════════════════════════════════════════════ - -/// Main capability invocation handler with two-level dispatch. -/// -/// First: single bit test to separate arch vs core -/// Then: smaller match within each category -/// -/// This is more branch-predictor friendly because: -/// 1. The arch bit test is highly predictable (most calls are core) -/// 2. Each sub-match has fewer cases -#[inline(never)] // Keep as separate function for branch prediction -pub fn handle_cap_invoke( - nucleus: &mut Nucleus, - cap_slot: u32, - op: u32, - args: &[u64; 6], -) -> Result<(u64, u64), CapError> { - let domain = nucleus.current_domain_mut()?; - let slot = KeySlot(cap_slot as u16); - let entry = domain.keytable.lookup_mut(slot)?; - let obj_type = entry.object_type(); - - if obj_type.is_arch() { - // Architecture-specific dispatch (less common path) - arch_invoke::(nucleus, entry, obj_type, op, args) - } else { - // Core dispatch (common path) - core_invoke::(nucleus, entry, obj_type, op, args) - } -} - -/// Core object dispatch -#[inline(always)] -fn core_invoke( - nucleus: &mut Nucleus, - entry: &mut KeyEntry, - obj_type: ObjectType, - op: u32, - args: &[u64; 6], -) -> Result<(u64, u64), CapError> { - let core_type = CoreType::try_from(obj_type)?; - - match core_type { - CoreType::Null => Err(CapError::NullCapability), - - // CoreType::Untyped => { - // let untyped = entry.as_object_mut::()?; - // // Untyped::invoke(untyped, ....) - // api::untyped::invoke(untyped, entry.rights(), op, args, &mut nucleus.pools) - // } - CoreType::DebugConsole => { - let debug_console = entry.as_object_mut::()?; - DebugConsole::invoke(debug_console, parampampam) - } // CoreType::Domain => { - // let domain = entry.as_object_mut::()?; - // api::domain::invoke(domain, entry.rights(), op, args) - // } - - // CoreType::KeyTable => { - // let kt = entry.as_object_mut::()?; - // api::keytable::invoke(kt, entry.rights(), op, args) - // } - - // CoreType::Notification => { - // let notify = entry.as_object_mut::()?; - // api::notification::invoke(notify, entry.rights(), entry.badge(), op, args) - // } - - // CoreType::EventCount => { - // let ec = entry.as_object_mut::()?; - // api::event_count::invoke(ec, entry.rights(), op, args) - // } - - // CoreType::Endpoint => { - // let ep = entry.as_object_mut::()?; - // api::endpoint::invoke(ep, entry.rights(), entry.badge(), op, args, nucleus) - // } - - // CoreType::Time => { - // let time = entry.as_object_mut::()?; - // api::time::invoke(time, entry.rights(), op, args, nucleus) - // } - - // CoreType::Buffer => { - // let buf = entry.as_object_mut::()?; - // api::buffer::invoke(buf, entry.rights(), op, args) - // } - - // CoreType::Reply => { - // let reply = entry.as_object_mut::()?; - // api::reply::invoke(reply, op, args, nucleus) - // } - } -} - -/// Architecture-specific dispatch - defined per architecture -#[inline(always)] -fn arch_invoke( - nucleus: &mut Nucleus, - entry: &mut KeyEntry, - obj_type: ObjectType, - op: u32, - args: &[u64; 6], -) -> Result<(u64, u64), CapError> { - let arch_type = ArchType::try_from(obj_type)?; - - match arch_type { - ArchType::Frame => { - let frame = entry.as_object_mut::()?; - A::invoke_frame(frame, entry.rights(), op, args, nucleus) // or A::Frame::invoke()? - } - - ArchType::PageTable => { - let pt = entry.as_object_mut::()?; - A::invoke_page_table(pt, entry.rights(), op, args, nucleus) - } - - ArchType::VSpace => { - let vspace = entry.as_object_mut::()?; - A::invoke_vspace(vspace, entry.rights(), op, args, nucleus) - } - - ArchType::ASIDPool => { - let pool = entry.as_object_mut::()?; - A::invoke_asid_pool(pool, entry.rights(), op, args, nucleus) - } - - ArchType::ASID => { - let asid = entry.as_object_mut::()?; - A::invoke_asid(asid, entry.rights(), op, args) - } - - ArchType::IOSpace => { - // May not be supported on all architectures - A::invoke_io_space(entry, op, args, nucleus) - } - - ArchType::IOPort => { - // x86 only - #[cfg(target_arch = "x86_64")] - { - let port = entry.as_object_mut::()?; - x86_64::invoke_io_port(port, entry.rights(), op, args) - } - #[cfg(not(target_arch = "x86_64"))] - { - Err(CapError::UnsupportedArchType(arch_type)) - } - } - - ArchType::IRQHandler => A::invoke_irq_handler(entry, op, args, nucleus), - - ArchType::IRQControl => A::invoke_irq_control(entry, op, args, nucleus), - } -} - fn get_pc() -> u64 { let pc: u64; unsafe { diff --git a/kernel/nucleus/src/api/arch/aarch64_objects.rs b/kernel/nucleus/src/objects/arch/aarch64_objects.rs similarity index 53% rename from kernel/nucleus/src/api/arch/aarch64_objects.rs rename to kernel/nucleus/src/objects/arch/aarch64_objects.rs index 2d9e32e29..9901a866f 100644 --- a/kernel/nucleus/src/api/arch/aarch64_objects.rs +++ b/kernel/nucleus/src/objects/arch/aarch64_objects.rs @@ -1,8 +1,17 @@ +use { + crate::api::{ + arch::frame::AArch64Frame, + object_type::{ArchType, ObjectType}, + }, + libmemory::{phys_addr::PhysAddr, virt_addr::VirtAddr}, + libsyscall::CapError, +}; + // ═══════════════════════════════════════════════════════════════════ // AARCH64 IMPLEMENTATION // ═══════════════════════════════════════════════════════════════════ -struct AArch64; +pub struct AArch64; impl ArchObjects for AArch64 { type Frame = AArch64Frame; @@ -94,70 +103,7 @@ impl ArchObjects for AArch64 { args: &[u64; 6], kernel: &mut Kernel, ) -> Result<(u64, u64), CapError> { - #[repr(u8)] - enum FrameOp { - Map = 0, - Unmap = 1, - GetAddress = 2, - Remap = 3, - } - - let op = match op { - 0 => FrameOp::Map, - 1 => FrameOp::Unmap, - 2 => FrameOp::GetAddress, - 3 => FrameOp::Remap, - _ => return Err(CapError::InvalidOperation), - }; - - match op { - FrameOp::Map => { - // args[0] = vspace_slot - // args[1] = virt_addr - // args[2] = rights (R/W/X bits) - // args[3] = attrs (cacheability, etc.) - - if !rights.contains(Rights::READ) { - return Err(CapError::InsufficientRights); - } - - let vspace_slot = KeySlot(args[0] as u16); - let virt_addr = VirtAddr::new(args[1]); - let map_rights = MapRights::from_bits(args[2] as u8); - let attrs = MemAttrs::from_bits(args[3] as u8); - - // Get the VSpace from the slot - let domain = kernel.current_domain()?; - let vspace_entry = domain.keytable.lookup(vspace_slot)?; - let vspace = vspace_entry.as_object::()?; - - // Perform the mapping - aarch64_map_frame(frame, vspace, virt_addr, map_rights, attrs, kernel)?; - - Ok((0, 0)) - } - - FrameOp::Unmap => { - if frame.map_count == 0 { - return Err(CapError::NotMapped); - } - // ... unmap logic - Ok((0, 0)) - } - - FrameOp::GetAddress => { - // Requires Grant right to expose physical address - if !rights.contains(Rights::GRANT) { - return Err(CapError::InsufficientRights); - } - Ok((frame.phys_addr.as_u64(), frame.size.size() as u64)) - } - - FrameOp::Remap => { - // Change attributes on existing mapping - todo!("frame remap") - } - } + crate::api::arch::frame::invoke(frame, rights, op, args) } // ───────────────────────────────────────────────────────────────── @@ -171,33 +117,7 @@ impl ArchObjects for AArch64 { args: &[u64; 6], kernel: &mut Kernel, ) -> Result<(u64, u64), CapError> { - #[repr(u8)] - enum PageTableOp { - Map = 0, // Map this PT into a parent PT or VSpace - Unmap = 1, // Unmap from parent - } - - let op = match op { - 0 => PageTableOp::Map, - 1 => PageTableOp::Unmap, - _ => return Err(CapError::InvalidOperation), - }; - - match op { - PageTableOp::Map => { - // args[0] = vspace_slot - // args[1] = virt_addr (determines which slot in parent) - let vspace_slot = KeySlot(args[0] as u16); - let virt_addr = VirtAddr::new(args[1]); - - // ... mapping logic - todo!("page_table map") - } - - PageTableOp::Unmap => { - todo!("page_table unmap") - } - } + // crate::api::arch::page_table::invoke(pt, rights, op, args) } // ───────────────────────────────────────────────────────────────── @@ -211,38 +131,7 @@ impl ArchObjects for AArch64 { args: &[u64; 6], kernel: &mut Kernel, ) -> Result<(u64, u64), CapError> { - #[repr(u8)] - enum VSpaceOp { - AssignASID = 0, - GetASID = 1, - } - - let op = match op { - 0 => VSpaceOp::AssignASID, - 1 => VSpaceOp::GetASID, - _ => return Err(CapError::InvalidOperation), - }; - - match op { - VSpaceOp::AssignASID => { - // args[0] = asid_pool_slot - let pool_slot = KeySlot(args[0] as u16); - - let domain = kernel.current_domain_mut()?; - let pool_entry = domain.keytable.lookup_mut(pool_slot)?; - let pool = pool_entry.as_object_mut::()?; - - let asid = pool.allocate().ok_or(CapError::ASIDPoolExhausted)?; - - vspace.asid = Some(asid); - Ok((asid as u64, 0)) - } - - VSpaceOp::GetASID => { - let asid = vspace.asid.ok_or(CapError::NoASIDAssigned)?; - Ok((asid as u64, 0)) - } - } + // crate::api::arch::vspace::invoke(vspace, rights, op, args) } // ───────────────────────────────────────────────────────────────── @@ -272,52 +161,3 @@ impl ArchObjects for AArch64 { Err(CapError::InvalidOperation) } } - -// ───────────────────────────────────────────────────────────────── -// AArch64 Frame (Physical Page) -// ───────────────────────────────────────────────────────────────── - -/// A physical memory frame on AArch64. -/// -/// Frames can be 4KB, 2MB, or 1GB and can be mapped into VSpaces. -#[derive(Debug)] -pub struct AArch64Frame { - /// Physical address (aligned to frame size) - phys_addr: PhysAddr, - /// Frame size - size: FrameSize, - /// Is this device memory? (affects cacheability) - is_device: bool, - /// Mapping count (for shared frames) - map_count: u16, -} - -impl AArch64Frame { - pub fn new(phys_addr: PhysAddr, size: FrameSize) -> Self { - // Verify alignment - debug_assert!(phys_addr.as_u64() & ((1 << size.bits()) - 1) == 0); - - Self { - phys_addr, - size, - is_device: false, - map_count: 0, - } - } - - pub fn phys_addr(&self) -> PhysAddr { - self.phys_addr - } - - pub fn size(&self) -> FrameSize { - self.size - } - - pub fn is_mapped(&self) -> bool { - self.map_count > 0 - } -} - -impl KernelObject for AArch64Frame { - const TYPE: ObjectType = ObjectType::Frame; -} diff --git a/kernel/nucleus/src/api/arch/arch_pools.rs b/kernel/nucleus/src/objects/arch/arch_pools.rs similarity index 100% rename from kernel/nucleus/src/api/arch/arch_pools.rs rename to kernel/nucleus/src/objects/arch/arch_pools.rs diff --git a/kernel/nucleus/src/objects/arch/frame.rs b/kernel/nucleus/src/objects/arch/frame.rs new file mode 100644 index 000000000..50b8d7a3a --- /dev/null +++ b/kernel/nucleus/src/objects/arch/frame.rs @@ -0,0 +1,50 @@ +// ───────────────────────────────────────────────────────────────── +// AArch64 Frame (Physical Page) +// ───────────────────────────────────────────────────────────────── + +use {crate::api::key_table::KeySlot, libmemory::virt_addr::VirtAddr, libsyscall::CapError}; + +/// A physical memory frame on AArch64. +/// +/// Frames can be 4KB, 2MB, or 1GB and can be mapped into VSpaces. +#[derive(Debug)] +pub struct AArch64Frame { + /// Physical address (aligned to frame size) + phys_addr: PhysAddr, + /// Frame size + size: FrameSize, + /// Is this device memory? (affects cacheability) + is_device: bool, + /// Mapping count (for shared frames) + map_count: u16, +} + +impl AArch64Frame { + pub fn new(phys_addr: PhysAddr, size: FrameSize) -> Self { + // Verify alignment + debug_assert!(phys_addr.as_u64() & ((1 << size.bits()) - 1) == 0); + + Self { + phys_addr, + size, + is_device: false, + map_count: 0, + } + } + + pub fn phys_addr(&self) -> PhysAddr { + self.phys_addr + } + + pub fn size(&self) -> FrameSize { + self.size + } + + pub fn is_mapped(&self) -> bool { + self.map_count > 0 + } +} + +impl NucleusObject for AArch64Frame { + const TYPE: ObjectType = ObjectType::Frame; +} diff --git a/kernel/nucleus/src/objects/arch/mod.rs b/kernel/nucleus/src/objects/arch/mod.rs new file mode 100644 index 000000000..52bbb603f --- /dev/null +++ b/kernel/nucleus/src/objects/arch/mod.rs @@ -0,0 +1,9 @@ +#[cfg(target_arch = "aarch64")] +pub mod aarch64_objects; +#[cfg(target_arch = "aarch64")] +pub use aarch64_objects::AArch64 as ArchObjectsImpl; + +#[cfg(target_arch = "aarch64")] +pub mod frame; +#[cfg(target_arch = "aarch64")] +pub use frame::AArch64Frame; diff --git a/kernel/nucleus/src/objects/arch/page_table.rs b/kernel/nucleus/src/objects/arch/page_table.rs new file mode 100644 index 000000000..e2fe3c313 --- /dev/null +++ b/kernel/nucleus/src/objects/arch/page_table.rs @@ -0,0 +1 @@ +pub struct AArch64PageTable; diff --git a/kernel/nucleus/src/arch_objects.rs b/kernel/nucleus/src/objects/arch_objects.rs similarity index 92% rename from kernel/nucleus/src/arch_objects.rs rename to kernel/nucleus/src/objects/arch_objects.rs index 79c9f726c..ab9550cff 100644 --- a/kernel/nucleus/src/arch_objects.rs +++ b/kernel/nucleus/src/objects/arch_objects.rs @@ -1,3 +1,5 @@ +use {crate::objects::NucleusObject, libobject::ArchType, libsyscall::CapError}; + // ═══════════════════════════════════════════════════════════════════ // ARCH OBJECTS TRAIT WITH INVOKE METHODS // ═══════════════════════════════════════════════════════════════════ @@ -5,11 +7,11 @@ /// Architecture abstraction trait - extended with invoke methods pub trait ArchObjects: Sized + 'static { // ─── Associated Types ─── - type Frame: KernelObject; - type PageTable: KernelObject; - type VSpace: KernelObject; - type ASIDPool: KernelObject; - type ASID: KernelObject; + type Frame: NucleusObject; + type PageTable: NucleusObject; + type VSpace: NucleusObject; + type ASIDPool: NucleusObject; + type ASID: NucleusObject; // ─── Constants ─── const FRAME_SIZES: &'static [FrameSize]; diff --git a/kernel/nucleus/src/objects/buffer.rs b/kernel/nucleus/src/objects/buffer.rs new file mode 100644 index 000000000..ad725b3dc --- /dev/null +++ b/kernel/nucleus/src/objects/buffer.rs @@ -0,0 +1,33 @@ +// ==================== +// == Nucleus object == +// ==================== + +/// Buffer kernel object - represents a contiguous memory region +pub struct Buffer { + phys_base: PhysAddr, // Physical address (fixed at creation) + size: usize, // Size in bytes (fixed at creation) + flags: BufferFlags, // CACHED, DEVICE, SHARED, etc. + + // Mapping tracking (for single-address-space) + // mappings: SmallVec, // Who has it mapped where +} + +struct Mapping { + domain_id: DomainId, + virt_addr: VirtAddr, + permissions: Rights, // May be less than cap rights (derived cap) +} + +bitflags::bitflags! { + pub struct BufferFlags: u32 { + const CACHED = 1 << 0; // Normal cacheable memory + const DEVICE = 1 << 1; // Device memory (uncached, no speculative) + const SHARED = 1 << 2; // Multi-domain sharing expected + const DMA = 1 << 3; // DMA-capable (physically contiguous) + const EXEC = 1 << 4; // Executable (if supported) + } +} + +impl NucleusObject for Buffer { + const TYPE: ObjectType = ObjectType::Buffer; +} diff --git a/kernel/nucleus/src/objects/debug_console.rs b/kernel/nucleus/src/objects/debug_console.rs new file mode 100644 index 000000000..d79617bb6 --- /dev/null +++ b/kernel/nucleus/src/objects/debug_console.rs @@ -0,0 +1,25 @@ +use {core::slice, libobject::ObjectType}; + +// ==================== +// == Nucleus object == +// ==================== + +struct DebugConsole; + +impl DebugConsole { + fn handle_write(ptr: u64, len: u64) -> Result<(), SyscallError> { + let slice = unsafe { slice::from_raw_parts(ptr as *const u8, len as usize) }; + let mut buf = [0u8; 4096]; + // SAFETY: Need to validate user pointer is valid, need to copy via kernel physmem mapping. + buf.copy_from_slice(slice); + buf[slice.len()] = 0; + let cstr = unsafe { core::ffi::CStr::from_bytes_with_nul(&buf[..=slice.len() + 1]) } + .map_err(SyscallError::Unknown)?; + libqemu::semihosting::sys_write0_call(cstr); + Ok(()) + } +} + +impl NucleusObject for DebugConsole { + const TYPE: ObjectType = ObjectType::DEBUG_CONSOLE; +} diff --git a/kernel/nucleus/src/objects/domain.rs b/kernel/nucleus/src/objects/domain.rs new file mode 100644 index 000000000..660b799b8 --- /dev/null +++ b/kernel/nucleus/src/objects/domain.rs @@ -0,0 +1,245 @@ +use core::sync::atomic::Ordering; + +// ==================== +// == Nucleus object == +// ==================== + +struct Domain; + +impl Domain { + // Initialize new domain's cspace + fn init_cspace(&mut self) { + // Slot 0: capability to this captbl itself + self.cspace[CAPTBL_SELF] = Cap::new(ObjectType::KeyTable, self.cspace_id); + // Now domain can manipulate its own caps + } +} + +#[repr(u32)] +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub enum DomainState { + /// Just created, never run + Inactive = 0, + /// Ready to receive CPU time + Runnable = 1, + /// Currently executing (only one domain per CPU) + Running = 2, + /// Waiting on notification/event/endpoint + Blocked = 3, + /// Explicitly suspended by parent + Suspended = 4, + /// Faulted, needs handler + Faulted = 5, +} + +#[repr(u32)] +#[derive(Copy, Clone, Debug)] +pub enum BlockReason { + None = 0, + Notification = 1, // NotifyCap::wait() + EventCount = 2, // EventCountCap::await_ge() + Endpoint = 3, // EndpointCap::call() or recv() + TimeDonated = 4, // Donated time, waiting for return +} + +// pub enum DeactivateReason { +// TimeExhausted, +// BlockedOnEvent(EndpointCap), <-- BlockReason::Endpoint +// Yielded, +// Faulted(fault), +// } + +/// Domain Control Block +/// Our DCB structure - combining Nemesis ideas with our capability model +/// +/// Key insight: split into kernel-private and shared sections +#[repr(C, align(128))] // Cache-line aligned +pub struct DomainControlBlock { + // ═══════════════════════════════════════════════════════════ + // SHARED SECTION (read-only mapped to userspace) + // ═══════════════════════════════════════════════════════════ + // ─── Identity ─── + pub id: DomainId, + pub name: [u8; 24], + + // ─── Execution State (Acquire/Release on state field) ─── + pub state: AtomicU32, // DomainState + pub block_reason: AtomicU32, // BlockReason (if blocked) + pub blocked_on: AtomicU32, // Cap slot we're blocked on + + // ─── Time Accounting (QoS) ─── + /// Cumulative CPU time consumed (nanoseconds) + pub time_used_ns: AtomicU64, + /// Time remaining in current activation + pub time_remaining_ns: AtomicU64, + /// Number of times activated + pub activation_count: AtomicU64, + + // ─── Event State ─── + pub pending_notifications: AtomicU64, // Bitmap of pending notify caps + /// Number of pending events (sum across all endpoints) + pub pending_events: AtomicU32, // Number of event counts with data + + /// Endpoint that caused last wakeup + pub last_event_ep: AtomicU32, + + // ─── Scheduling Parameters ─── + /// Parent scheduler domain + pub scheduler: DomainId, + /// Scheduling priority/parameters + pub priority: u32, + /// Allocation period (for periodic domains) + pub period_ns: u64, + /// CPU allocation per period + pub budget_ns: u64, // slice_ns + + /// Scheduled deadline (absolute time) + pub deadline: AtomicU64, + + // ─── Fault Information ─── + /// Last fault type (if any) + pub fault_type: AtomicU32, + + /// Fault address + pub fault_addr: AtomicU64, + + pub fault_cap: AtomicU32, // Cap slot that caused fault + + // Padding to cache line + _pad: [u8; 16], + // ═══════════════════════════════════════════════════════════ + // PRIVATE SECTION (kernel only, NOT mapped to userspace) + // ═══════════════════════════════════════════════════════════ + // + // This would be in a separate structure or after a page boundary + // - Saved register context + // - Capability space root + // - Kernel stack pointer + // - Etc. +} + +// Verify size for cache alignment +const _: () = assert!(core::mem::size_of::() == 128); + +// 32 DCBs per 4KB page +// Multiple pages for more domains + +// Userspace sees: const DCB_BASE: *const DomainControlBlock = 0xFFFF_0000_0000_0000; // FIXME: pervasives +// Access DCB n: &*DCB_BASE.add(n) + +/// Userspace view of DCB array +/// Mapped read-only at a well-known address +pub struct DcbView { + base: *const DomainControlBlock, +} + +impl DcbView { + /// Get from well-known address (set up by kernel at domain creation) + pub const fn new() -> Self { + Self { + base: 0xFFFF_0000_0000_0000 as *const DomainControlBlock, + } + } + + /// Read any domain's state + #[inline(always)] + pub fn get(&self, id: DomainId) -> &DomainControlBlock { + unsafe { &*self.base.add(id.0 as usize) } + } + + /// Get my own DCB + #[inline(always)] + pub fn myself(&self) -> &DomainControlBlock { + // Current domain ID stored in thread-local or well-known register + self.get(current_domain_id()) + } +} + +// Domain scheduling support in kernel: +impl Nucleus { + /// Called when domain is activated (receives CPU time) + fn activate_domain(&mut self, id: DomainId, time_budget: u64) { + let dcb = self.dcb_mut(id); + + dcb.state + .store(DomainState::Running as u32, Ordering::Release); + dcb.time_remaining_ns.store(time_budget, Ordering::Release); + dcb.deadline.store(now() + time_budget, Ordering::Release); + } + + /// Called on every context switch FROM this domain + fn deactivate_domain(&mut self, id: DomainId, reason: DeactivateReason) { + let dcb = self.dcb_mut(id); + let elapsed = /* calculate from timer */0; + + // Update time accounting + dcb.time_used_ns.fetch_add(elapsed, Ordering::Relaxed); + dcb.time_remaining_ns.fetch_sub(elapsed, Ordering::Relaxed); + + // Update state + match reason { + DeactivateReason::TimeExhausted => { + dcb.state + .store(DomainState::Runnable as u32, Ordering::Release); + } + DeactivateReason::BlockedOnEvent(ep) => { + dcb.state + .store(DomainState::Blocked as u32, Ordering::Release); + dcb.block_reason + .store(BlockReason::Event as u32, Ordering::Release); + dcb.last_event_ep.store(ep, Ordering::Release); + } + DeactivateReason::Yielded => { + dcb.state + .store(DomainState::Runnable as u32, Ordering::Release); + } + DeactivateReason::Faulted(fault) => { + dcb.state + .store(DomainState::Faulted as u32, Ordering::Release); + dcb.fault_type.store(fault.type_code(), Ordering::Release); + dcb.fault_addr.store(fault.addr(), Ordering::Release); + } + } + } + + /// Called when event arrives for blocked domain + fn signal_domain(&mut self, id: DomainId) { + let dcb = self.dcb_mut(id); + + dcb.pending_events.fetch_add(1, Ordering::Release); + + // If blocked on events, make runnable + if dcb.state.load(Ordering::Acquire) == DomainState::Blocked as u32 { + dcb.state + .store(DomainState::Runnable as u32, Ordering::Release); + } + } +} + +// ## Memory Ordering Considerations + +// KERNEL (writer) USERSPACE (reader) +// ─────────────── ────────────────── + +// // Update multiple fields +// dcb.time_used.store(x, Relaxed); +// dcb.time_remaining.store(y, Relaxed); +// dcb.state.store(z, Release); ──────────────────────┐ +// │ │ +// │ Release ensures all │ +// │ prior writes visible │ +// ▼ ▼ +// let state = dcb.state.load(Acquire); +// // Acquire ensures we see +// // all writes before the Release +// let used = dcb.time_used.load(Relaxed); +// let rem = dcb.time_remaining.load(Relaxed); + +// Protocol: +// - Kernel does Release store on state LAST +// - Userspace does Acquire load on state FIRST +// - Then can safely read other fields with Relaxed + +impl NucleusObject for Domain { + const TYPE: ObjectType = ObjectType::Domain; +} diff --git a/kernel/nucleus/src/objects/endpoint.rs b/kernel/nucleus/src/objects/endpoint.rs new file mode 100644 index 000000000..4b4bb30cc --- /dev/null +++ b/kernel/nucleus/src/objects/endpoint.rs @@ -0,0 +1,196 @@ +// ==================== +// == Nucleus object == +// ==================== + +/// Message passed through endpoints +/// Fits in registers for fast IPC (~200-500 cycles) +#[repr(C)] +pub struct Message { + /// Message label/opcode - receiver dispatches on this + pub label: u64, + + /// 5 general-purpose data words + pub data: [u64; 5], + + /// Optional capability to transfer (None = no transfer) + /// Sender's cap is moved (not copied) to receiver + pub cap: Option, +} + +impl Message { + pub const fn new(label: u64) -> Self { + Self { + label, + data: [0; 5], + cap: None, + } + } + + pub fn with_data(label: u64, d0: u64, d1: u64, d2: u64, d3: u64, d4: u64) -> Self { + Self { + label, + data: [d0, d1, d2, d3, d4], + cap: None, + } + } + + pub fn with_cap(mut self, cap_slot: CapSlot) -> Self { + self.cap = Some(cap_slot); + self + } +} + +/// Kernel object +struct Endpoint { + /// Domain that can receive on this endpoint (None = anyone) + receiver: Option, + + /// Current state + state: EndpointState, + + /// Waiting senders (if state == Recving, this is empty) + /// Waiting receivers (if state == Sending, this is empty) + wait_queue: WaitQueue, + + /// Message buffer (kernel memory) + msg_regs: [u64; 6], + + /// Badge of sender (filled when message delivered) + sender_badge: u64, + + /// Cap transfer slot + transfer_cap: Option, +} + +enum EndpointState { + Idle, + /// Someone is blocked sending + Sending, + /// Someone is blocked receiving + Recving, +} + +impl Endpoint { + fn handle_call( + &mut self, + caller: &mut Domain, + msg: &[u64; 6], + cap_slot: Option, + badge: u64, + ) -> SyscallResult { + match self.state { + EndpointState::Recving => { + // Receiver waiting! Fast path - direct switch + let receiver = self.wait_queue.pop_front().unwrap(); + + // Copy message to receiver + self.msg_regs = *msg; + self.sender_badge = badge; + self.transfer_cap = cap_slot; + + // Block caller waiting for reply + caller.state = DomainState::Blocked; + caller.block_reason = BlockReason::Endpoint; + + // Wake receiver + receiver.state = DomainState::Runnable; + + // Switch to receiver (donate remaining time) + switch_to(receiver); + + Ok(0) + } + + EndpointState::Idle | EndpointState::Sending => { + // No receiver - block caller + caller.state = DomainState::Blocked; + caller.block_reason = BlockReason::Endpoint; + + self.wait_queue.push_back(caller.id); + self.state = EndpointState::Sending; + + // Store message for when receiver arrives + self.msg_regs = *msg; + self.sender_badge = badge; + self.transfer_cap = cap_slot; + + // Schedule someone else + schedule_next(); + + Ok(0) + } + } + } + + fn handle_recv(&mut self, receiver: &mut Domain, reply_dest_slot: CapSlot) -> SyscallResult { + match self.state { + EndpointState::Sending => { + let sender_info = self.wait_queue.pop_front().unwrap(); + + // Create Reply object for this call + let reply = Reply { + caller: sender_info.domain_id, + state: ReplyState::Pending, + }; + + // Allocate kernel memory for Reply object - FIXME: kernel memory allocation! + // and place capability in receiver's cspace + let reply_cap = kernel_alloc_reply(reply)?; + receiver.cspace.insert(reply_dest_slot, reply_cap)?; + + // Copy message to receiver + receiver.regs.x0 = 0; // Success + receiver.regs.x1 = sender_info.badge; + receiver.regs.x2 = self.msg_regs[0]; // label + receiver.regs.x3 = self.msg_regs[1]; + receiver.regs.x4 = self.msg_regs[2]; + receiver.regs.x5 = self.msg_regs[3]; + receiver.regs.x6 = self.msg_regs[4]; + receiver.regs.x7 = self.msg_regs[5]; + // x8 = transferred cap slot (if any) + + if self.wait_queue.is_empty() { + self.state = EndpointState::Idle; + } + + Ok(0) + } + + EndpointState::Idle | EndpointState::Recving => { + // Block receiver + receiver.state = DomainState::Blocked; + receiver.block_reason = BlockReason::Endpoint; + receiver.blocked_data = reply_dest_slot as u64; // Remember where to put reply cap + + self.wait_queue.push_back(receiver.id); + self.state = EndpointState::Recving; + + schedule_next(); + Ok(0) + } + } + } + + fn handle_reply_recv( + &mut self, + server: &mut Domain, + reply_cap_slot: CapSlot, + next_reply_slot: CapSlot, + reply_msg: &[u64; 6], + ) -> SyscallResult { + // 1. Send reply via the provided reply cap + let reply_cap = server.cspace.take(reply_cap_slot)?; + let reply = reply_cap.as_reply()?; + reply.handle_send(reply_msg, None)?; + + // Reply object is consumed, free kernel memory + kernel_free_reply(reply); + + // 2. Receive next message + self.handle_recv(server, next_reply_slot) + } +} + +impl NucleusObject for Endpoint { + const TYPE: ObjectType = ObjectType::Endpoint; +} diff --git a/kernel/nucleus/src/objects/event_count.rs b/kernel/nucleus/src/objects/event_count.rs new file mode 100644 index 000000000..607744500 --- /dev/null +++ b/kernel/nucleus/src/objects/event_count.rs @@ -0,0 +1,12 @@ +// ==================== +// == Nucleus object == +// ==================== + +struct EventCount { + value: u64, // Monotonically increasing counter + waiters: WaitQueue, // Domains waiting for value >= target +} + +impl NucleusObject for EventCount { + const TYPE: ObjectType = ObjectType::EventCount; +} diff --git a/kernel/nucleus/src/objects/key_table.rs b/kernel/nucleus/src/objects/key_table.rs new file mode 100644 index 000000000..9fa629aef --- /dev/null +++ b/kernel/nucleus/src/objects/key_table.rs @@ -0,0 +1,100 @@ +// ==================== +// == Nucleus object == +// ==================== + +/// A capability table for a domain. +/// +/// This is what seL4 calls a CNode. Each domain has one. +/// The table itself is a kernel object that can be referenced +/// by capabilities (for capability space manipulation). +pub struct KeyTable { + /// The actual capability entries + entries: [KeyEntry; Self::NUM_SLOTS], + /// Domain that owns this table + owner: DomainId, + /// Number of valid entries (for iteration) + count: u16, +} + +impl KeyTable { + /// Number of slots per table (power of 2 for fast indexing) + pub const NUM_SLOTS: usize = 256; + + /// Create a new empty capability table + pub fn new(owner: DomainId) -> Self { + Self { + entries: [const { KeyEntry::null() }; Self::NUM_SLOTS], + owner, + count: 0, + } + } + + /// Lookup a capability by slot index + #[inline] + pub fn lookup(&self, slot: KeySlot) -> Result<&KeyEntry, CapError> { + let idx = slot.0 as usize; + if idx >= Self::NUM_SLOTS { + return Err(CapError::InvalidSlot(slot)); + } + + let entry = &self.entries[idx]; + if !entry.is_valid() { + return Err(CapError::EmptySlot(slot)); + } + + Ok(entry) + } + + /// Lookup a capability mutably + #[inline] + pub fn lookup_mut(&mut self, slot: KeySlot) -> Result<&mut KeyEntry, CapError> { + let idx = slot.0 as usize; + if idx >= Self::NUM_SLOTS { + return Err(CapError::InvalidSlot(slot)); + } + + let entry = &mut self.entries[idx]; + if !entry.is_valid() { + return Err(CapError::EmptySlot(slot)); + } + + Ok(entry) + } + + /// Insert a capability at a specific slot + pub fn insert(&mut self, slot: KeySlot, entry: KeyEntry) -> Result<(), CapError> { + let idx = slot.0 as usize; + if idx >= Self::NUM_SLOTS { + return Err(CapError::InvalidSlot(slot)); + } + + if self.entries[idx].is_valid() { + return Err(CapError::SlotOccupied(slot)); + } + + self.entries[idx] = entry; + self.count += 1; + Ok(()) + } + + /// Remove a capability from a slot + pub fn remove(&mut self, slot: KeySlot) -> Result { + let idx = slot.0 as usize; + if idx >= Self::NUM_SLOTS { + return Err(CapError::InvalidSlot(slot)); + } + + let entry = core::mem::replace(&mut self.entries[idx], KeyEntry::null()); + if entry.is_valid() { + self.count -= 1; + Ok(entry) + } else { + Err(CapError::EmptySlot(slot)) + } + } +} + +// KeyTable is itself a kernel object +impl NucleusObject for KeyTable { + const TYPE: ObjectType = ObjectType::KeyTable; +} diff --git a/kernel/nucleus/src/objects/mod.rs b/kernel/nucleus/src/objects/mod.rs new file mode 100644 index 000000000..1391da047 --- /dev/null +++ b/kernel/nucleus/src/objects/mod.rs @@ -0,0 +1,6 @@ +// pub mod arch; +pub mod arch_objects; +pub mod nucleus_object; +pub mod object_ref; + +pub use {arch::ArchObjectsImpl, arch_objects::ArchObjects, nucleus_object::NucleusObject}; diff --git a/kernel/nucleus/src/objects/notification.rs b/kernel/nucleus/src/objects/notification.rs new file mode 100644 index 000000000..ba46e3ef3 --- /dev/null +++ b/kernel/nucleus/src/objects/notification.rs @@ -0,0 +1,22 @@ +// ==================== +// == Nucleus object == +// ==================== + +struct Notification { + state: u64, // Bitmap + waiters: WaitQueue, // Blocked domains + bound: Option, // Optional bound domain for fast wakeup +} + +impl Notification { + fn signal(&mut self, bits: u64) {} + fn wait() { + // if bits are already set, clear and immediately return + // otherwise block the domain.. + } + fn poll() {} +} + +impl NucleusObject for Notification { + const TYPE: ObjectType = ObjectType::Notification; +} diff --git a/kernel/nucleus/src/nucleus.rs b/kernel/nucleus/src/objects/nucleus.rs similarity index 100% rename from kernel/nucleus/src/nucleus.rs rename to kernel/nucleus/src/objects/nucleus.rs diff --git a/kernel/nucleus/src/nucleus_object.rs b/kernel/nucleus/src/objects/nucleus_object.rs similarity index 74% rename from kernel/nucleus/src/nucleus_object.rs rename to kernel/nucleus/src/objects/nucleus_object.rs index 40f4d2b60..4960a88a2 100644 --- a/kernel/nucleus/src/nucleus_object.rs +++ b/kernel/nucleus/src/objects/nucleus_object.rs @@ -6,8 +6,6 @@ //! 3. Objects live in typed pools (good for allocation) //! 4. Support for derivation/revocation tree -use core::ptr::NonNull; - // ┌─────────────────────────────────────────────────────────────────────┐ // │ ARCHITECTURE-SPECIFIC OBJECTS │ // ├─────────────────────────────────────────────────────────────────────┤ @@ -83,89 +81,10 @@ use core::ptr::NonNull; /// Marker trait for kernel objects - provides type → ObjectType mapping pub trait NucleusObject: Sized + 'static { - const TYPE: ObjectType; + const TYPE: libobject::ObjectType; //TODO: add invoke here? // fn invoke(obj: &Self::TYPE, op: u32, args: &[u64]) -> SyscallResult; } -// ═══════════════════════════════════════════════════════════════════ -// TYPE-ERASED OBJECT POINTER -// ═══════════════════════════════════════════════════════════════════ - -/// A type-erased pointer to a kernel object, with its type tag. -/// -/// This is the "fat pointer" alternative - we store the type alongside -/// the pointer so we can safely cast it back. -#[derive(Clone, Copy)] -pub struct ObjectRef { - ptr: NonNull<()>, - obj_type: ObjectType, -} - -impl ObjectRef { - /// Create a new object reference from a typed pointer - pub fn new(obj: &T) -> Self { - Self { - ptr: NonNull::from(obj).cast(), - obj_type: T::TYPE, - } - } - - /// Create from a mutable pointer (for objects in pools) - /// - /// # Safety - /// Caller must ensure the pointer is valid and properly aligned - pub unsafe fn from_raw(ptr: *mut T) -> Self { - Self { - ptr: NonNull::new_unchecked(ptr.cast()), - obj_type: T::TYPE, - } - } - - /// Get the object type - #[inline] - pub fn object_type(&self) -> ObjectType { - self.obj_type - } - - /// Attempt to cast to a specific type (immutable) - #[inline] - pub fn try_as(&self) -> Option<&T> { - if self.obj_type == T::TYPE { - // SAFETY: We verified the type matches - Some(unsafe { self.ptr.cast::().as_ref() }) - } else { - None - } - } - - /// Attempt to cast to a specific type (mutable) - #[inline] - pub fn try_as_mut(&mut self) -> Option<&mut T> { - if self.obj_type == T::TYPE { - // SAFETY: We verified the type matches - Some(unsafe { self.ptr.cast::().as_mut() }) - } else { - None - } - } - - /// Cast with error on type mismatch - #[inline] - pub fn as_type(&self) -> Result<&T, CapError> { - self.try_as().ok_or(CapError::TypeMismatch { - expected: T::TYPE, - found: self.obj_type, - }) - } - - /// Cast with error on type mismatch (mutable) - #[inline] - pub fn as_type_mut(&mut self) -> Result<&mut T, CapError> { - self.try_as_mut().ok_or(CapError::TypeMismatch { - expected: T::TYPE, - found: self.obj_type, - }) - } -} +// Should object type live here or in libobject? Seems like here is a better place but CapErrors need to refer to object types. diff --git a/kernel/nucleus/src/object_pool.rs b/kernel/nucleus/src/objects/object_pool.rs similarity index 100% rename from kernel/nucleus/src/object_pool.rs rename to kernel/nucleus/src/objects/object_pool.rs diff --git a/kernel/nucleus/src/objects/object_ref.rs b/kernel/nucleus/src/objects/object_ref.rs new file mode 100644 index 000000000..60a54e547 --- /dev/null +++ b/kernel/nucleus/src/objects/object_ref.rs @@ -0,0 +1,85 @@ +use { + super::NucleusObject, crate::api::object_type::ObjectType, core::ptr::NonNull, + libsyscall::CapError, +}; + +// ═══════════════════════════════════════════════════════════════════ +// TYPE-ERASED OBJECT POINTER +// ═══════════════════════════════════════════════════════════════════ + +/// A type-erased pointer to a kernel object, with its type tag. +/// +/// This is the "fat pointer" alternative - we store the type alongside +/// the pointer so we can safely cast it back. +#[derive(Clone, Copy)] +pub struct ObjectRef { + ptr: NonNull<()>, + obj_type: ObjectType, +} + +impl ObjectRef { + /// Create a new object reference from a typed pointer + pub fn new(obj: &T) -> Self { + Self { + ptr: NonNull::from(obj).cast(), + obj_type: T::TYPE, + } + } + + /// Create from a mutable pointer (for objects in pools) + /// + /// # Safety + /// Caller must ensure the pointer is valid and properly aligned + pub unsafe fn from_raw(ptr: *mut T) -> Self { + Self { + ptr: NonNull::new_unchecked(ptr.cast()), + obj_type: T::TYPE, + } + } + + /// Get the object type + #[inline] + pub fn object_type(&self) -> ObjectType { + self.obj_type + } + + /// Attempt to cast to a specific type (immutable) + #[inline] + pub fn try_as(&self) -> Option<&T> { + if self.obj_type == T::TYPE { + // SAFETY: We verified the type matches + Some(unsafe { self.ptr.cast::().as_ref() }) + } else { + None + } + } + + /// Attempt to cast to a specific type (mutable) + #[inline] + pub fn try_as_mut(&mut self) -> Option<&mut T> { + if self.obj_type == T::TYPE { + // SAFETY: We verified the type matches + Some(unsafe { self.ptr.cast::().as_mut() }) + } else { + None + } + } + + /// Cast with error on type mismatch + #[inline] + pub fn as_type(&self) -> Result<&T, CapError> { + self.try_as().ok_or(CapError::TypeMismatch { + expected: T::TYPE, + found: self.obj_type, + }) + } + + /// Cast with error on type mismatch (mutable) + #[inline] + pub fn as_type_mut(&mut self) -> Result<&mut T, CapError> { + self.try_as_mut().ok_or(CapError::TypeMismatch { + expected: T::TYPE, + found: self.obj_type, + }) + } +} diff --git a/kernel/nucleus/src/objects/reply.rs b/kernel/nucleus/src/objects/reply.rs new file mode 100644 index 000000000..4c4d11bf5 --- /dev/null +++ b/kernel/nucleus/src/objects/reply.rs @@ -0,0 +1,70 @@ +// ==================== +// == Nucleus object == +// ==================== + +/// Reply kernel object +struct Reply { + /// Domain waiting for this reply + caller: DomainId, + + /// State of this reply object + state: ReplyState, +} + +#[derive(Clone, Copy, PartialEq)] +enum ReplyState { + /// Caller is blocked waiting + Pending, + /// Reply was sent, object is consumed + Used, + /// Caller cancelled or timed out + Cancelled, +} + +impl Reply { + fn handle_send(&mut self, msg: &[u64; 6], cap_slot: Option) -> SyscallResult { + match self.state { + ReplyState::Pending => { + let caller = get_domain_mut(self.caller); + + // Copy reply to caller's registers + caller.regs.x0 = 0; // Success + caller.regs.x1 = msg[0]; // label + caller.regs.x2 = msg[1]; + caller.regs.x3 = msg[2]; + caller.regs.x4 = msg[3]; + caller.regs.x5 = msg[4]; + caller.regs.x6 = msg[5]; + + // Transfer cap if present + if let Some(slot) = cap_slot { + let cap = current_domain().cspace.take(slot)?; + caller.cspace.insert(RECEIVED_CAP_SLOT, cap)?; + caller.regs.x7 = RECEIVED_CAP_SLOT as u64; + } else { + caller.regs.x7 = u64::MAX; + } + + // Wake caller + caller.state = DomainState::Runnable; + + // Mark reply object as used (one-shot) + self.state = ReplyState::Used; + + Ok(0) + } + + ReplyState::Used => Err(SyscallError::ReplyAlreadyUsed), + + ReplyState::Cancelled => { + // Caller gave up - just consume the reply cap + self.state = ReplyState::Used; + Ok(0) // Not an error, just no-op + } + } + } +} + +impl NucleusObject for Reply { + const TYPE: ObjectType = ObjectType::Reply; +} diff --git a/kernel/nucleus/src/objects/time.rs b/kernel/nucleus/src/objects/time.rs new file mode 100644 index 000000000..0f845dad0 --- /dev/null +++ b/kernel/nucleus/src/objects/time.rs @@ -0,0 +1,13 @@ +// ==================== +// == Nucleus object == +// ==================== + +struct Time { + remaining_us: u64, // Microseconds left + deadline: Instant, // When this slice expires + parent: Option, // For custom revocation tree (to easily return unused time to parent) +} + +impl NucleusObject for TimeSlice { + const TYPE: ObjectType = ObjectType::Time; +} diff --git a/kernel/nucleus/src/objects/untyped.rs b/kernel/nucleus/src/objects/untyped.rs new file mode 100644 index 000000000..3affc0f86 --- /dev/null +++ b/kernel/nucleus/src/objects/untyped.rs @@ -0,0 +1,297 @@ +// The key insight: an Untyped capability (in keytable) IS the kernel object. +// There's no separate in-kernel structure — just the capability itself carrying all the metadata. +// This is extremely minimal! + +// ==================== +// == Nucleus object == +// ==================== + +// ┌─────────────────────────────────────────────────────────────────────┐ +// │ cap_untyped_cap (64-bit systems) │ +// ├──────────────────┬──────────────────────────────────────────────────┤ +// │ capType (4 bits)│ = cap_untyped_cap │ +// ├──────────────────┼──────────────────────────────────────────────────┤ +// │ capPtr │ Physical address of untyped region │ +// ├──────────────────┼──────────────────────────────────────────────────┤ +// │ capBlockSize │ Total size in bits (region = 2^capBlockSize) │ +// ├──────────────────┼──────────────────────────────────────────────────┤ +// │ capFreeIndex │ Watermark: next free byte offset (>> MIN_BITS) │ +// ├──────────────────┼──────────────────────────────────────────────────┤ +// │ capIsDevice │ Boolean: is this device memory (not normal RAM)? │ +// └──────────────────┴──────────────────────────────────────────────────┘ + +/// State of an untyped memory region - FIXME prolly not needed +#[repr(u8)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum UntypedState { + /// Fresh, no children created + Free = 0, + /// Has been split into children + HasChildren = 1, + /// Has been retyped to a typed object + Retyped = 2, +} + +/// Represents a region of physical memory that can be retyped +/// FIXME: This should be an overlay type of KeyEntry, directly in the KeyTable! +#[repr(C, align(64))] +pub struct Untyped { + /// Physical base address of the memory region + pub paddr: PhysAddr, + /// Size as log2 (actual size = 1 << size_bits) + pub size_bits: u8, + /// Current state + pub state: UntypedState, + /// Number of child untypeds (if split) + pub child_count: u16, + /// Watermark: offset of next free byte within region + /// Only valid when state == Free + pub watermark: u64, +} + +const _: () = assert!(size_of::() <= 64); + +// ═══════════════════════════════════════════════════════════════════ +// EXTENDED RETYPE WITH ARCH OBJECTS +// ═══════════════════════════════════════════════════════════════════ + +// UNTYPED REGION (2^capBlockSize bytes) +// ┌────────────────────────────────────────────────────────────┐ +// │█████████████████████████░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░│ +// │ ALLOCATED │ FREE │ +// └────────────────────────────────────────────────────────────┘ +// ↑ ↑ ↑ +// capPtr capPtr + FREE_INDEX_TO_OFFSET(capFreeIndex) capPtr + 2^capBlockSize +// (watermark) + +// VALIDATION: +// Type valid? Size within bounds? +// Device untyped → only Frames/Untypeds allowed +// num ≤ CONFIG_RETYPE_FAN_OUT_LIMIT (default 256)? +// Destination slots empty? +// +// CALCULATE ALIGNED FREE POINTER +// +// freeRef = GET_FREE_REF(capPtr, freeIndex) +// objectSize = 1 << size_bits +// alignedFreeRef = ROUND_UP(freeRef, objectSize) ← CRITICAL! +// +// (alignment can waste memory — the "fragmentation" issue) +// +// CHECK SUFFICIENT SPACE +// +// untypedFreeBytes = regionEnd - alignedFreeRef +// if (untypedFreeBytes >> objectSizeBits) < num: +// return seL4_NotEnoughMemory (with memoryLeft in MR) +// +// RESET CHECK (if children were revoked) +// +// if (cap_untyped_cap_get_capFreeIndex(cap) != 0 +// && children_revoked): +// • Zero memory (preemptible!) +// • Reset capFreeIndex to 0 +// +// CREATE OBJECTS +// +// for i in 0..num: +// objectAddr = alignedFreeRef + (i * objectSize) +// cap = createObject(type, objectAddr, size_bits, ...) +// insert_cap_into_cnode(destSlots[i], cap) +// establish_CDT_relationship(untypedCap → newCap) +// +// UPDATE WATERMARK +// +// newFreeIndex = OFFSET_TO_FREE_INDEX( +// alignedFreeRef + num*objectSize - capPtr +// ) +// cap_untyped_cap_set_capFreeIndex(cap, newFreeIndex) + +impl Untyped { + /// Retype this untyped memory into a nucleus object. + /// + /// Handles both core and architecture-specific object types. + pub fn retype( + &mut self, + obj_type: ObjectType, + size_bits: u8, + dest_slot: KeySlot, + dest_keytable: &mut KeyTable, + pools: &mut NucleusPools, + ) -> Result<(), CapError> { + // Determine object size based on type + let obj_size = if obj_type.is_core() { + core_object_size(obj_type, size_bits)? + } else { + A::validate_retype(obj_type, size_bits)? + }; + + // Check we have enough memory + if self.watermark + obj_size > self.size { + return Err(CapError::InsufficientMemory); + } + + // Allocate from untyped + let obj_addr = self.phys_addr.offset(self.watermark); + self.watermark += obj_size; + + // Create the object based on type + let entry = if obj_type.is_core() { + self.create_core_object(obj_type, obj_addr, size_bits, pools)? + } else { + self.create_arch_object::(obj_type, obj_addr, size_bits, pools)? + }; + + // Insert capability into destination slot + dest_keytable.insert(dest_slot, entry)?; + + Ok(()) + } + + fn create_core_object( + &mut self, + obj_type: ObjectType, + phys_addr: PhysAddr, + size_bits: u8, + pools: &mut NucleusPools, + ) -> Result { + match obj_type { + ObjectType::Notification => { + let notify = Notification::new(); + let obj = pools + .notifications + .allocate(notify) + .ok_or(CapError::PoolExhausted)?; + Ok(KeyEntry::new(obj, Rights::all(), 0, None)) + } + + ObjectType::EventCount => { + let ec = EventCount::new(); + let obj = pools + .event_counts + .allocate(ec) + .ok_or(CapError::PoolExhausted)?; + Ok(KeyEntry::new(obj, Rights::all(), 0, None)) + } + + ObjectType::Domain => { + let domain = Domain::new(phys_addr); + let obj = pools + .domains + .allocate(domain) + .ok_or(CapError::PoolExhausted)?; + Ok(KeyEntry::new(obj, Rights::all(), 0, None)) + } + + ObjectType::Time => { + let time = TimeSlice::new_default(); + let obj = pools + .time_slices + .allocate(time) + .ok_or(CapError::PoolExhausted)?; + Ok(KeyEntry::new(obj, Rights::all(), 0, None)) + } + + ObjectType::Endpoint => { + let ep = Endpoint::new(); + let obj = pools + .endpoints + .allocate(ep) + .ok_or(CapError::PoolExhausted)?; + Ok(KeyEntry::new(obj, Rights::all(), 0, None)) + } + + ObjectType::Buffer => { + let size = 1usize << size_bits; + let buffer = Buffer::new(phys_addr, size); + let obj = pools + .buffers + .allocate(buffer) + .ok_or(CapError::PoolExhausted)?; + Ok(KeyEntry::new(obj, Rights::READ | Rights::WRITE, 0, None)) + } + + ObjectType::KeyTable => { + let kt = KeyTable::new_empty(); + let obj = pools + .keytables + .allocate(kt) + .ok_or(CapError::PoolExhausted)?; + Ok(KeyEntry::new(obj, Rights::all(), 0, None)) + } + + ObjectType::Untyped => { + // Split: create a child untyped + let child_size = 1usize << size_bits; + let child = Untyped { + phys_addr, + size: child_size, + watermark: 0, + is_device: self.is_device, + }; + let obj = pools + .untypeds + .allocate(child) + .ok_or(CapError::PoolExhausted)?; + Ok(KeyEntry::new(obj, Rights::all(), 0, None)) + } + + ObjectType::Reply => { + let reply = Reply::new(); + let obj = pools + .replies + .allocate(reply) + .ok_or(CapError::PoolExhausted)?; + // Reply caps have restricted rights + Ok(KeyEntry::new(obj, Rights::WRITE, 0, None)) + } + + _ => Err(CapError::InvalidObjectType), + } + } + + fn create_arch_object( + &mut self, + obj_type: ObjectType, + phys_addr: PhysAddr, + size_bits: u8, + pools: &mut NucleusPools, + ) -> Result { + let obj_ref = A::create_arch_object(obj_type, phys_addr, size_bits, &mut pools.arch)?; + + // Default rights based on type + let rights = match obj_type { + ObjectType::Frame => Rights::READ | Rights::WRITE, + ObjectType::PageTable => Rights::all(), + ObjectType::VSpace => Rights::all(), + ObjectType::ASIDPool => Rights::all(), + ObjectType::ASID => Rights::all(), + _ => Rights::all(), + }; + + Ok(KeyEntry::from_ref(obj_ref, rights, 0, None)) + } +} + +fn core_object_size(obj_type: ObjectType, size_bits: u8) -> Result { + match obj_type { + ObjectType::Notification => Ok(core::mem::size_of::()), + ObjectType::EventCount => Ok(core::mem::size_of::()), + ObjectType::Time => Ok(core::mem::size_of::()), + ObjectType::Endpoint => Ok(core::mem::size_of::()), + ObjectType::Reply => Ok(core::mem::size_of::()), + ObjectType::Domain => Ok(4096), + ObjectType::KeyTable => Ok(core::mem::size_of::()), + ObjectType::Buffer | ObjectType::Untyped => { + if size_bits > 30 { + Err(CapError::InvalidSize) + } else { + Ok(1usize << size_bits) + } + } + _ => Err(CapError::InvalidObjectType), + } +} + +impl NucleusObject for Untyped { + const TYPE: ObjectType = ObjectType::Untyped; +} diff --git a/libs/object/Cargo.toml b/libs/object/Cargo.toml new file mode 100644 index 000000000..d0dc14080 --- /dev/null +++ b/libs/object/Cargo.toml @@ -0,0 +1,25 @@ +[package] +name = "libobject" + +description = "Kernel objects interface" + +authors = { workspace = true } +categories = { workspace = true } +documentation = { workspace = true } +edition = { workspace = true } +homepage = { workspace = true } +license = { workspace = true } +readme = { workspace = true } +repository = { workspace = true } +version = { workspace = true } + +publish = false + +[badges] +maintenance = { status = "experimental" } + +[dependencies] +libsyscall = { workspace = true } + +[lints] +workspace = true diff --git a/libs/object/src/arch/asid_pool.rs b/libs/object/src/arch/asid_pool.rs new file mode 100644 index 000000000..71470c687 --- /dev/null +++ b/libs/object/src/arch/asid_pool.rs @@ -0,0 +1,5 @@ +#[repr(u8)] +pub enum ASIDPoolOp { + /// Allocate an ASID from this pool + Allocate = 0, +} diff --git a/libs/object/src/arch/frame.rs b/libs/object/src/arch/frame.rs new file mode 100644 index 000000000..99a930340 --- /dev/null +++ b/libs/object/src/arch/frame.rs @@ -0,0 +1,11 @@ +#[repr(u8)] +pub enum FrameOp { + /// Map frame into a VSpace at given virtual address + Map = 0, + /// Unmap frame from VSpace + Unmap = 1, + /// Get physical address (requires special rights) + GetAddress = 2, + /// Remap with different attributes + Remap = 3, +} diff --git a/libs/object/src/arch/page_table.rs b/libs/object/src/arch/page_table.rs new file mode 100644 index 000000000..ae0ad0a9e --- /dev/null +++ b/libs/object/src/arch/page_table.rs @@ -0,0 +1,7 @@ +#[repr(u8)] +pub enum PageTableOp { + /// Map this page table into parent table or VSpace + Map = 0, + /// Unmap from parent + Unmap = 1, +} diff --git a/libs/object/src/arch/vspace.rs b/libs/object/src/arch/vspace.rs new file mode 100644 index 000000000..1914c1d7a --- /dev/null +++ b/libs/object/src/arch/vspace.rs @@ -0,0 +1,11 @@ +#[repr(u8)] +pub enum VSpaceOp { + /// Assign root page table + SetRoot = 0, + /// Assign ASID + AssignASID = 1, + /// Activate (switch to this address space) + Activate = 2, + /// Get current ASID + GetASID = 3, +} diff --git a/libs/object/src/buffer.rs b/libs/object/src/buffer.rs new file mode 100644 index 000000000..0dfd34d65 --- /dev/null +++ b/libs/object/src/buffer.rs @@ -0,0 +1,191 @@ +// ================================================== +// == Public user interface, usable from userspace == +// ================================================== + +/// Buffer capability with permission tracking in type system +pub struct BufferKey { + key: Key, + size: usize, + _perm: PhantomData

, +} + +/// Buffer operations via protected_call +#[repr(u8)] +pub enum BufferOp { + Map = 0, // Map into caller's address space + Unmap = 1, // Remove mapping + Query = 2, // Get buffer info (size, flags, mapping status) +} + +pub trait Permission {} +pub struct ReadOnly; +pub struct ReadWrite; +impl Permission for ReadOnly {} +impl Permission for ReadWrite {} + +impl BufferKey

{ + /// Map buffer into current domain's address space. + /// + /// # Arguments + /// * `hint` - Optional preferred virtual address (kernel may ignore) + /// * `flags` - Mapping flags (caching behavior, etc.) + /// + /// # Returns + /// Virtual address where buffer is mapped + pub fn map(&self, hint: Option, flags: MapFlags) -> Result { + let hint_val = hint.map(|a| a.as_u64()).unwrap_or(0); + + let ret = unsafe { + protected_call2( + self.key.slot as u64, + BufferOp::Map as u64, + hint_val, + // self.size, ? + flags.bits() as u64, + ) + }; + + if ret == 0 { + Err(Error::MapFailed) + } else { + Ok(VirtAddr::new(ret)) + } + } + + /// Unmap buffer from current domain's address space. + pub fn unmap(&self) -> Result<(), Error> { + let ret = unsafe { protected_call1(self.key.slot as u64, BufferOp::Unmap as u64, 0) }; + Error::from_code(ret) + } + + /// Query buffer information (no mapping required) + pub fn query(&self) -> Result { + let mut info = BufferInfo::default(); + let ret = unsafe { + protected_call2( + self.key.slot as u64, + BufferOp::Query as u64, + &mut info as *mut _ as u64, + 0, + ) + }; + if ret == 0 { + Ok(info) + } else { + Err(Error::from_code(ret)) + } + } +} + +// Other ops +impl BufferKey

{ + /// Size of this buffer + #[inline] + pub fn size(&self) -> usize { + self.size + } +} + +impl BufferKey { + /// Derive read-only capability (like &T from &mut T) + pub fn derive_readonly(&self, dest_slot: u32) -> Result, Error> { + let ret = unsafe { + protected_call3( + CAPTBL_SELF, + KeyTableOp::CopyDerive, + self.key.slot as u64, + dest_slot as u64, + Rights::READ.bits() as u64, + ) + }; + if ret == 0 { + Ok(BufferKey { + key: Cap::new(dest_slot), + size: self.size, + _perm: PhantomData, + }) + } else { + Err(Error::from_code(ret)) + } + } + + /// Map and get mutable slice + pub fn map_slice_mut(&self) -> Result, Error> { + let addr = self.map(None, MapFlags::NONE)?; + Ok(MappedSliceMut { + cap: self, + ptr: addr.as_mut_ptr(), + len: self.size, + }) + } +} + +impl BufferKey { + /// Map and get immutable slice + pub fn map_slice(&self) -> Result, Error> { + let addr = self.map(None, MapFlags::NONE)?; + Ok(MappedSlice { + cap: self, + ptr: addr.as_ptr(), + len: self.size, + }) + } +} + +#[derive(Default)] +pub struct BufferInfo { + pub size: usize, + pub flags: BufferFlags, + pub is_mapped: bool, + pub mapped_addr: Option, +} + +bitflags::bitflags! { + pub struct MapFlags: u32 { + const NONE = 0; + const FIXED = 1 << 0; // Fail if hint can't be used + const POPULATE = 1 << 1; // Pre-fault all pages + const UNCACHED = 1 << 2; // Override to uncached + } +} + +/// RAII guard that unmaps on drop +pub struct MappedSlice<'a> { + cap: &'a BufferKey, + ptr: *const u8, + len: usize, +} + +impl<'a> MappedSlice<'a> { + pub fn as_slice(&self) -> &[u8] { + unsafe { core::slice::from_raw_parts(self.ptr, self.len) } + } +} + +impl Drop for MappedSlice<'_> { + fn drop(&mut self) { + let _ = self.cap.unmap(); + } +} + +pub struct MappedSliceMut<'a> { + cap: &'a BufferKey, + ptr: *mut u8, + len: usize, +} + +impl<'a> MappedSliceMut<'a> { + pub fn as_slice(&self) -> &[u8] { + unsafe { core::slice::from_raw_parts(self.ptr, self.len) } + } + + pub fn as_mut_slice(&mut self) -> &mut [u8] { + unsafe { core::slice::from_raw_parts_mut(self.ptr, self.len) } + } +} + +impl Drop for MappedSliceMut<'_> { + fn drop(&mut self) { + let _ = self.cap.unmap(); + } +} diff --git a/libs/object/src/debug_console.rs b/libs/object/src/debug_console.rs new file mode 100644 index 000000000..3e6d06742 --- /dev/null +++ b/libs/object/src/debug_console.rs @@ -0,0 +1,32 @@ +use crate::{CapError, Key}; + +// ================================================== +// == Public user interface, usable from userspace == +// ================================================== + +pub struct DebugConsoleKey { + key: Key, +} + +enum DebugConsoleType {} + +#[repr(u8)] +pub enum DebugConsoleOp { + /// Capability invocation to write a message to a debug console. + Write = 0, +} + +// Root domain gets a DebugConsoleCap, can delegate to others +impl DebugConsoleKey { + pub fn write(&self, s: &str) -> Result<(), CapError> { + let (_ok, _, _) = unsafe { + libsyscall::protected_call2( + self.key.slot(), + DebugConsoleOp::Write as u32, + s.as_ptr() as u64, + s.len() as u64, + ) + }; + Ok(()) + } +} diff --git a/libs/object/src/domain.rs b/libs/object/src/domain.rs new file mode 100644 index 000000000..2f4ca8111 --- /dev/null +++ b/libs/object/src/domain.rs @@ -0,0 +1,91 @@ +// ================================================== +// == Public user interface, usable from userspace == +// ================================================== + +#[repr(u8)] +pub enum DomainOp { + Activate = 0, // Make domain runnable + Grant = 1, // Grant capability to domain + Suspend = 2, // Suspend domain + Resume = 3, // Resume suspended domain +} + +/// Domain capability - handle to a protection domain. +/// State queries use shared DCB (no syscall), mutations use CapInvoke. +pub struct DomainKey { + cap: Key, + id: DomainId, +} + +impl DomainKey { + /// Create a new domain from untyped memory. + /// Convenience wrapper around UntypedRetype. + pub fn create(untyped: &mut UntypedCap, dest_slot: KeySlot) -> Result { + // Domains need ~4KB (12 bits) for kernel structures + untyped.retype( + untyped.split(12)?, // Carve off 4KB + ObjectType::Domain, + 12, + dest_slot, + )?; + + // Domain ID is returned in secondary return value + // (or we query it from the newly created DCB) + Ok(DomainKey { + cap: Cap::new(dest_slot), + id: DomainId(/* ... */), + }) + } + + /// Get domain state from shared DCB + #[inline] + pub fn state(&self) -> DomainState { + let dcb = DCB.get(self.id); + DomainState::try_from(dcb.state.load(Ordering::Acquire)).unwrap_or(DomainState::Inactive) + } + + /// Get time used from shared DCB + #[inline] + pub fn time_used_ns(&self) -> u64 { + let dcb = DCB.get(self.id); + dcb.time_used_ns.load(Ordering::Relaxed) + } + + /// Get pending notifications from shared DCB (NO SYSCALL!) + #[inline] + pub fn pending_notifications(&self) -> u64 { + let dcb = DCB.get(self.id); + dcb.pending_notifications.load(Ordering::Relaxed) + } + + /// Activate domain (make runnable) - requires syscall + pub fn activate(&self) -> Result<(), Error> { + let ret = unsafe { syscall3(self.cap.slot as u64, DomainOp::Activate as u64, 0) }; + Error::from_code(ret) + } + + /// Grant a capability to this domain - requires syscall + pub fn grant(&self, cap: &Cap, dest_slot: CapSlot) -> Result<(), Error> { + let ret = unsafe { + syscall4( + self.cap.slot as u64, + DomainOp::Grant as u64, + cap.slot() as u64, + dest_slot as u64, + ) + }; + Error::from_code(ret) + } + + /// Suspend domain - requires syscall + pub fn suspend(&self) -> Result<(), Error> { + let ret = unsafe { syscall3(self.cap.slot as u64, DomainOp::Suspend as u64, 0) }; + Error::from_code(ret) + } + + /// Resume suspended domain - requires syscall + pub fn resume(&self) -> Result<(), Error> { + let ret = unsafe { syscall3(self.cap.slot as u64, DomainOp::Resume as u64, 0) }; + Error::from_code(ret) + } +} diff --git a/libs/object/src/endpoint.rs b/libs/object/src/endpoint.rs new file mode 100644 index 000000000..d8c8baaca --- /dev/null +++ b/libs/object/src/endpoint.rs @@ -0,0 +1,252 @@ +// Endpoint capability operations +// +// Endpoints enable synchronous call/return IPC with direct domain switch. +// Unlike Notifications (fire-and-forget) or EventCounts (streaming), +// Endpoints are for request/response patterns. + +use {crate::key::Key, libsyscall::protected_call4}; + +// ================================================== +// == Public user interface, usable from userspace == +// ================================================== + +/// Endpoint capability for synchronous IPC +/// +/// Two "views" of the same endpoint: +/// - Client holds EndpointKey (can Call/Send) +/// - Server holds EndpointKey with Recv rights (can Recv/Reply) +/// +/// Badges identify which client is calling (granted during cap derivation) +/// +/// With explicit Reply objects: +/// - recv() returns (badge, msg, ReplyCap) +/// - reply is done via ReplyCap::send(), not endpoint method +/// - reply_recv() takes a ReplyCap to consume +pub struct EndpointKey { + key: Key, +} + +enum EndpointType {} + +#[repr(u8)] +pub enum EndpointOp { + /// Send message and wait for reply (client operation) + /// Blocks until server receives, processes, and replies + Call = 0, + + /// Send message without waiting (non-blocking) + /// If no receiver waiting, message is dropped (returns error) + Send = 1, + + /// Wait for incoming message (server operation) + /// Blocks until a sender calls + Recv = 2, + + /// Reply to caller and wait for next message (server fast-path) + /// Combines Reply + Recv in single syscall (seL4 optimization) + ReplyRecv = 3, + + /// Reply to caller without waiting for next + Reply = 4, + + /// Forward call to another endpoint (proxy pattern) + /// Transfers the reply capability to the new endpoint + Forward = 5, +} + +impl EndpointKey { + // ═══════════════════════════════════════════════════════════════════ + // CLIENT OPERATIONS + // ═══════════════════════════════════════════════════════════════════ + + /// Call: send message and block waiting for reply + /// + /// This is the primary client→server operation. + /// Performs direct domain switch to receiver (fast path). + /// + /// Returns the reply message (label + data + optional cap) + pub fn call(&self, msg: &Message) -> Result { + let (ret0, ret1, ret2, ret3, ret4, ret5, ret6) = unsafe { + syscall_ipc( + self.cap.slot as u64, + EndpointOp::Call as u64, + msg.label, + msg.data[0], + msg.data[1], + msg.data[2], + msg.data[3], + msg.data[4], + msg.cap.map(|s| s as u64).unwrap_or(u64::MAX), + ) + }; + + if ret0 == 0 { + Ok(Message { + label: ret1, + data: [ret2, ret3, ret4, ret5, 0], + cap: if ret6 != u64::MAX { + Some(ret6 as CapSlot) + } else { + None + }, + }) + } else { + Err(IpcError::from_code(ret0)) + } + } + + /// Send: non-blocking send (no reply expected) + /// + /// If receiver is waiting → message delivered, returns Ok + /// If no receiver → message dropped, returns Err(WouldBlock) + pub fn send(&self, msg: &Message) -> Result<(), IpcError> { + let ret = unsafe { + protected_call4( + self.cap.slot as u64, + EndpointOp::Send as u64, + msg.label, + msg.data[0], + msg.data[1], + msg.data[2], + ) + }; + IpcError::from_code(ret.0) + } + + // ═══════════════════════════════════════════════════════════════════ + // SERVER OPERATIONS + // ═══════════════════════════════════════════════════════════════════ + + /// Recv: block waiting for incoming Call + /// + /// Returns (sender_badge, message, reply_cap) + /// Badge identifies which client called (set during cap derivation) + /// + /// The ReplyCap MUST be used to reply - it's consumed on use. + /// Dropping it without replying will send an error to the caller. + pub fn recv(&self, reply_slot: CapSlot) -> Result<(u64, Message, ReplyCap), IpcError> { + let (ret0, badge, label, d0, d1, d2, d3, d4, cap_slot) = unsafe { + syscall_ipc_recv( + self.cap.slot as u64, + EndpointOp::Recv as u64, + reply_slot as u64, // Where kernel places ReplyCap + ) + }; + + if ret0 == 0 { + let msg = Message { + label, + data: [d0, d1, d2, d3, d4], + cap: if cap_slot != u64::MAX { + Some(cap_slot as CapSlot) + } else { + None + }, + }; + + // Kernel placed ReplyCap in reply_slot + let reply_cap = ReplyCap { + cap: Cap::new(reply_slot), + }; + + Ok((badge, msg, reply_cap)) + } else { + Err(IpcError::from_code(ret0)) + } + } + + /// ReplyRecv: consume reply cap, send reply, AND wait for next message + /// + /// This is the server fast-path: one syscall does Reply + Recv. + /// + /// Takes the ReplyCap to consume (must reply to previous caller) + /// Returns new (badge, msg, reply_cap) for next caller + pub fn reply_recv( + &self, + reply_cap: ReplyCap, + reply_msg: &Message, + next_reply_slot: CapSlot, + ) -> Result<(u64, Message, ReplyCap), IpcError> { + let (ret0, badge, label, d0, d1, d2, d3, d4, cap_slot) = unsafe { + syscall_ipc_reply_recv( + self.cap.slot as u64, + EndpointOp::ReplyRecv as u64, + reply_cap.cap.slot as u64, // Reply cap to consume + next_reply_slot as u64, // Where to place next ReplyCap + reply_msg.label, + reply_msg.data[0], + reply_msg.data[1], + reply_msg.data[2], + reply_msg.data[3], + ) + }; + + // reply_cap consumed by kernel + core::mem::forget(reply_cap); + + if ret0 == 0 { + let msg = Message { + label, + data: [d0, d1, d2, d3, d4], + cap: if cap_slot != u64::MAX { + Some(cap_slot as CapSlot) + } else { + None + }, + }; + + let next_reply = ReplyCap { + cap: Cap::new(next_reply_slot), + }; + + Ok((badge, msg, next_reply)) + } else { + Err(IpcError::from_code(ret0)) + } + } + + /// Forward: pass this call to another endpoint (proxy pattern) + /// + /// Useful for: capability-filtered proxies, load balancers, etc. + /// The forwarded call appears to come from us (our badge) + pub fn forward(&self, target: &EndpointKey, msg: &Message) -> Result<(), IpcError> { + let ret = unsafe { + protected_call4( + self.cap.slot as u64, + EndpointOp::Forward as u64, + target.cap.slot as u64, + msg.label, + msg.data[0], + msg.data[1], + ) + }; + IpcError::from_code(ret.0) + } +} + +impl EndpointKey { + /// Derive a client capability with a specific badge + /// + /// The badge is returned to the server on recv(), identifying the caller. + /// This is how servers distinguish between clients. + pub fn derive_client(&self, badge: u64, dest_slot: CapSlot) -> Result { + let (ret, _, _) = unsafe { + protected_call4( + CAPTBL_SELF, // Support deriving directly into a client keytable? + KeyTableOp::CopyDerive, + self.key.slot() as u64, + dest_slot as u64, + Rights::CALL.bits() as u64, // Client can only Call, not Recv + badge, + ) + }; + + if ret == 0 { + Ok(EndpointKey { + cap: Cap::new(dest_slot), + }) + } else { + Err(Error::from_code(ret)) + } + } +} diff --git a/libs/object/src/event_count.rs b/libs/object/src/event_count.rs new file mode 100644 index 000000000..ff49fbdae --- /dev/null +++ b/libs/object/src/event_count.rs @@ -0,0 +1,73 @@ +// ================================================== +// == Public user interface, usable from userspace == +// ================================================== + +/// Event count capability - monotonic Reed-Kanodia counter for exact event tracking. +/// Best for: streaming, flow control, producer-consumer coordination. +/// +/// Unlike notifications, every advance() is counted - no coalescing. +/// This lets consumers know exactly how far behind they are. +pub struct EventCountKey { + key: Key, +} + +enum EventCountType {} + +#[repr(u8)] +pub enum EventCountOp { + Advance = 0, + Await = 1, + Read = 2, +} + +impl EventCountKey { + /// Advance: atomically increment counter by delta. + /// Returns new value. Never blocks. ~30 cycles. + #[inline] + pub fn advance(&self, delta: u64) -> u64 { + protected_call1(self.key.slot as u64, EventOp::Advance as u64, delta) + } + + /// Await: block until value >= target. + /// Returns current value (may be > target if producer is fast). + #[inline] + pub fn await_ge(&self, target: u64) -> u64 { + protected_call1(self.key.slot as u64, EventOp::Await as u64, target) + } + + /// Read: get current value without blocking. + #[inline] + pub fn read(&self) -> u64 { + protected_call0(self.key.slot as u64, EventOp::Read) + } +} + +/// Helper: tracks consumer position for a single reader +pub struct EventCountReader { + ec: EventCountKey, + last_seen: u64, +} + +impl EventCountReader { + pub fn new(ec: EventCountKey) -> Self { + let initial = ec.read(); + Self { + ec, + last_seen: initial, + } + } + + /// Wait for next event(s), returns count since last wait + pub fn wait_next(&mut self) -> u64 { + let target = self.last_seen + 1; + let current = self.ec.await_ge(target); + let delta = current - self.last_seen; + self.last_seen = current; + delta + } + + /// Check how many events pending without blocking + pub fn pending(&self) -> u64 { + self.ec.read() - self.last_seen + } +} diff --git a/kernel/nucleus/src/api/key.rs b/libs/object/src/key.rs similarity index 86% rename from kernel/nucleus/src/api/key.rs rename to libs/object/src/key.rs index 73ddfe3a2..30302f9de 100644 --- a/kernel/nucleus/src/api/key.rs +++ b/libs/object/src/key.rs @@ -4,11 +4,11 @@ use core::marker::PhantomData; // == Public user interface, usable from userspace == // ================================================== -/// Capability slot index - typed for safety +/// Capability slot index - strongly typed #[derive(Copy, Clone, Debug, PartialEq, Eq)] #[repr(transparent)] pub struct Key { - slot: u32, + slot: u32, // FIXME: KeySlot() _marker: PhantomData, } diff --git a/libs/object/src/key_table.rs b/libs/object/src/key_table.rs new file mode 100644 index 000000000..8e01fdf4f --- /dev/null +++ b/libs/object/src/key_table.rs @@ -0,0 +1,115 @@ +use { + crate::{CapError, Key, Rights}, + libsyscall::{protected_call1, protected_call4}, +}; + +// ================================================== +// == Public user interface, usable from userspace == +// ================================================== + +/// Slot index in a KeyTable +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +#[repr(transparent)] +pub struct KeySlot(pub u32); + +impl KeySlot { + pub const NULL: KeySlot = KeySlot(0); + pub const SELF_DOMAIN: KeySlot = KeySlot(1); + pub const PARENT_DOMAIN: KeySlot = KeySlot(2); + // CSpace layout with self-reference + pub const CAPTBL_SELF: KeySlot = KeySlot(3); // Every domain has cap to own captbl here - or rather to KeyMaster + // ... other well-known slots +} + +pub struct KeyTableKey { + key: Key, +} + +enum KeyTableType {} + +#[repr(u8)] +pub enum KeyTableOp { + CopyDerive = 0, // copy cap between slots or create derived cap with reduced rights + Move = 1, // move cap between slots + Delete = 2, // delete cap at slot + Revoke = 4, // revoke all children of cap +} + +// Userspace KeyMaster must track parent→child relationships, +// kernel only manages flat key tables. + +impl KeyTableKey { + // This naturally supports cross-domain derivation: + // "Create a read-only view of my buffer in their cspace" + // derive(&my_captbl, buffer_slot, &their_captbl, their_slot, Rights::READ)?; + /// Copy with derivation in single syscall + pub fn copy_derive( + &self, + src_slot: u32, + dst_captbl: &KeyTableKey, // Could be same or different! + dst_slot: u32, + rights: Rights, + ) -> Result<(), CapError> { + let (_ok, _, _) = unsafe { + protected_call4( + self.key.slot(), + KeyTableOp::CopyDerive as u32, + src_slot as u64, + dst_captbl.key.slot() as u64, + dst_slot as u64, + rights.bits(), + ) + }; + Ok(()) + } + + // fn activate(&self, slot: u32, object: NucleusObject) -> Result<()> { + // let captbl = self.get_captbl_mut()?; + // // SAFETY: User specifies slot, but kernel validates + // if slot >= captbl.len() { + // return Err(Error::SlotOutOfRange); + // } + // if captbl.slots[slot].is_valid() { + // return Err(Error::SlotOccupied); // User's bookkeeping was wrong + // } + // // Kernel creates the cap - user never touches this + // captbl.slots[slot] = Cap::new(object); + // Ok(()) + // } + + fn r#move() {} + + fn delete(&mut self, slot: u32) -> Result<(), CapError> { + // TODO: Must invoke on self-captbl cap + let (_ok, _, _) = + unsafe { protected_call1(self.key.slot(), KeyTableOp::Delete as u32, slot as u64) }; + Ok(()) + } + + // Revoke all children of cap in slot + fn revoke(&self, _captbl: &KeyTableKey, slot: u32) -> Result<(), CapError> { + let (_ok, _, _) = + unsafe { protected_call1(self.key.slot(), KeyTableOp::Revoke as u32, slot as u64) }; + Ok(()) + } + + // User code to copy cap to another domain (if you have their captbl cap): + fn grant_to( + &self, + my_slot: u32, + their_captbl: &KeyTableKey, + their_slot: u32, + ) -> Result<(), CapError> { + let (_ok, _, _) = unsafe { + protected_call4( + self.key.slot(), + KeyTableOp::CopyDerive as u32, + my_slot as u64, + their_captbl.key.slot() as u64, + their_slot as u64, + same_rights as u64, + ) + }; + Ok(()) + } +} diff --git a/libs/object/src/lib.rs b/libs/object/src/lib.rs new file mode 100644 index 000000000..102a2c49a --- /dev/null +++ b/libs/object/src/lib.rs @@ -0,0 +1,45 @@ +#![no_std] + +pub mod debug_console; +pub mod key; +pub mod key_table; +pub mod object_type; +pub mod rights; + +pub use { + key::Key, + key_table::KeySlot, + object_type::{ArchType, CoreType, ObjectType}, + rights::Rights, +}; + +pub type SyscallResult = core::result::Result<(u64, u64), CapError>; + +pub enum CapError { + Unknown, + InvalidPointer, + InsufficientRights, + NotMapped, + AlreadyMapped, + InvalidOperation, + ASIDPoolExhausted, + NoASIDAssigned, + InvalidSlot(KeySlot), + EmptySlot(KeySlot), + SlotOccupied(KeySlot), + NotCoreType(ObjectType), + UnknownCoreType(u8), + NotArchType(ObjectType), + UnknownArchType(u8), + UnsupportedArchType(u8), + InsufficientMemory, + PoolExhausted, + InvalidObjectType(ObjectType), + InvalidSize(usize), + NullCapability, + InvalidFrameSize(usize), + TypeMismatch { + expected: ObjectType, + found: ObjectType, + }, +} diff --git a/libs/object/src/notification.rs b/libs/object/src/notification.rs new file mode 100644 index 000000000..7a22e6199 --- /dev/null +++ b/libs/object/src/notification.rs @@ -0,0 +1,45 @@ +use { + crate::key::Key, + libsyscall::{protected_call0, protected_call1}, +}; + +// ================================================== +// == Public user interface, usable from userspace == +// ================================================== + +/// Notification capability - bitmap-based async signaling. +/// Best for: IRQs, completion events, wakeups. +pub struct NotificationKey { + key: Key, +} + +enum NotificationType {} + +#[repr(u8)] +pub enum NotifyOp { + Signal = 0, + Wait = 1, + Poll = 2, +} + +impl NotificationKey { + /// Signal: atomic OR into bitmap (always non-blocking) + /// Multiple signals to same bit coalesce. + #[inline] + pub fn signal(&self, bits: u64) -> Result<()> { + protected_call1(self.key.slot(), NotifyOp::Signal as u32, bits)?; + Ok(()) + } + + /// Wait: block until any bit set, returns + clears ALL bits + #[inline] + pub fn wait(&self) -> Result { + protected_call0(self.key.slot(), NotifyOp::Wait as u32) + } + + /// Poll: non-blocking check, returns + clears bits + #[inline] + pub fn poll(&self) -> u64 { + protected_call0(self.key.slot(), NotifyOp::Poll as u32) + } +} diff --git a/kernel/nucleus/src/api/object_type.rs b/libs/object/src/object_type.rs similarity index 97% rename from kernel/nucleus/src/api/object_type.rs rename to libs/object/src/object_type.rs index 9c4ae0e2b..a860fa6a5 100644 --- a/kernel/nucleus/src/api/object_type.rs +++ b/libs/object/src/object_type.rs @@ -1,3 +1,5 @@ +use crate::CapError; + /// Object type discriminant with architectural bit. /// /// Bit 7 (high bit) indicates architecture-specific type. @@ -104,7 +106,7 @@ impl TryFrom for CoreType { #[inline] fn try_from(ot: ObjectType) -> Result { if ot.is_arch() { - return Err(CapError::NotCoreType); + return Err(CapError::NotCoreType(ot)); } match ot.index() { 0 => Ok(CoreType::Null), @@ -151,7 +153,7 @@ impl TryFrom for ArchType { #[inline] fn try_from(ot: ObjectType) -> Result { if !ot.is_arch() { - return Err(CapError::NotArchType); + return Err(CapError::NotArchType(ot)); } match ot.index() { 0 => Ok(ArchType::Frame), diff --git a/libs/object/src/reply.rs b/libs/object/src/reply.rs new file mode 100644 index 000000000..bdbb19122 --- /dev/null +++ b/libs/object/src/reply.rs @@ -0,0 +1,93 @@ +// ================================================== +// == Public user interface, usable from userspace == +// ================================================== + +/// Reply capability - one-shot reply to a blocked caller. +/// +/// Created by kernel when a Call arrives, consumed when reply is sent. +/// This is a LINEAR type - must be used exactly once (or explicitly dropped). +/// +/// Making reply explicit enables: +/// - Async reply (store reply cap, reply later) +/// - Delegation (pass reply cap to helper domain) +/// - Multiple outstanding calls (each has own reply cap) +pub struct ReplyKey { + key: Key, +} + +enum ReplyType {} + +#[repr(u8)] +pub enum ReplyOp { + Send = 0, // Send reply message + SendWithCap = 1, // Send reply with cap transfer + SendError = 2, // Send error (used by Drop) +} + +impl ReplyKey { + /// Send reply to the blocked caller + /// + /// Consumes self - reply cap is one-shot! + /// After this, the caller is unblocked with the response. + pub fn send(self, msg: &Message) -> Result<(), IpcError> { + let ret = unsafe { + protected_call4( + self.key.slot(), + ReplyOp::Send as u64, + msg.label, + msg.data[0], + msg.data[1], + msg.data[2], + ) + }; + + // Consumed - don't run Drop (kernel already invalidated) + core::mem::forget(self); + + IpcError::from_code(ret.0) + } + + /// Send reply with capability transfer + pub fn send_with_key(self, msg: &Message, key: KeySlot) -> Result<(), IpcError> { + let ret = unsafe { + syscall_ipc_reply( + self.key.slot(), + ReplyOp::SendWithCap as u64, + msg.label, + msg.data[0], + msg.data[1], + msg.data[2], + msg.data[3], + msg.data[4], + key as u64, + ) + }; + + // Consumed - don't run Drop (kernel already invalidated) + core::mem::forget(self); + + IpcError::from_code(ret.0) + } +} + +/// Dropping a ReplyKey without sending is an ERROR for the caller. +/// The caller would remains blocked forever (or until timeout/cancellation). +/// +/// In debug builds, we panic. In release, we send an error reply. +impl Drop for ReplyKey { + fn drop(&mut self) { + // Reply cap dropped without sending - this is usually a bug! + // Send error reply to unblock caller + unsafe { + protected_call1( + self.key.slot(), + ReplyOp::SendError as u64, + IpcError::ReplyDropped as u64, + ); + } + + // Panic in debug mode to detect misuse. + #[cfg(debug_assertions)] + panic!("ReplyKey dropped without sending reply!"); + } +} diff --git a/libs/object/src/rights.rs b/libs/object/src/rights.rs new file mode 100644 index 000000000..0aa7fc2b9 --- /dev/null +++ b/libs/object/src/rights.rs @@ -0,0 +1,18 @@ +pub type Rights = u8; + +impl Rights { + pub const READ: u8 = 0x1; + pub const WRITE: u8 = 0x2; + pub const MAP: u8 = 0x2; + pub const SEND: u8 = 0x2; + pub const RECV: u8 = 0x1; + pub const CALL: u8 = 0x4; + pub const GRANT: u8 = 0x8; + + pub fn empty() -> Rights { + 0 + } + pub fn all() -> Rights { + 0xF + } +} diff --git a/libs/object/src/time.rs b/libs/object/src/time.rs new file mode 100644 index 000000000..9ef88ca8e --- /dev/null +++ b/libs/object/src/time.rs @@ -0,0 +1,66 @@ +// ================================================== +// == Public user interface, usable from userspace == +// ================================================== + +/// Time capability — represents a slice of CPU time +pub struct TimeKey { + key: Key, +} + +enum TimeType {} + +#[repr(u8)] +pub enum TimeOp { + Donate = 0, + Split = 1, + Merge = 2, + Query = 3, +} + +impl TimeKey { + /// Donate this time slice to target domain. + /// Current domain blocks until target exhausts time or yields. + pub fn donate(self, target: DomainKey) -> Result<(), SchedError> { + let (ret, _, _) = + unsafe { protected_call1(self.key.slot(), TimeOp::Donate as u64, target.key.slot()) }; + // Note: self is consumed (moved) - FIXME + Error::from_code(ret) + } + + /// Split off 'amount_us' into a new TimeCap. + /// Self retains the remainder. + pub fn split(&mut self, amount_us: u64) -> Result { + let new_slot = self.alloc_slot()?; // FIXME: In-kernel memory allocation? + let (ret, _, _) = unsafe { + protected_call2( + self.key.slot(), + TimeOp::Split as u64, + amount_us, + new_slot as u64, + ) + }; + if ret == 0 { + Ok(TimeKey { + key: Key::new(new_slot), + }) + } else { + Err(SchedError::from_code(ret)) + } + } + + /// Query remaining time in microseconds. + pub fn remaining(&self) -> u64 { + let (_, us, _) = unsafe { protected_call0(self.key.slot(), TimeOp::Query as u64) }; + us + } +} + +// Yield is just dropping + returning to scheduler +impl Drop for TimeKey { + fn drop(&mut self) { + // Kernel: return remaining time to parent in derivation tree + unsafe { + protected_call1(KeySlot::CAPTBL_SELF, KeyTableOp::Delete, self.key.slot()); + } + } +} diff --git a/libs/object/src/untyped.rs b/libs/object/src/untyped.rs new file mode 100644 index 000000000..cce81659b --- /dev/null +++ b/libs/object/src/untyped.rs @@ -0,0 +1,85 @@ +// ================================================== +// == Public user interface, usable from userspace == +// ================================================== + +pub struct UntypedKey { + key: Key, +} + +enum UntypedType {} + +#[repr(u8)] +pub enum UntypedOp { + Retype = 0, +} + +// Errors that can occur during retype operation +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[repr(u32)] +pub enum RetypeError { + /// Invalid object type specified + InvalidObjectType = 1, + /// Requested size is invalid for object type + InvalidSize = 2, + /// Not enough memory remaining in untyped + InsufficientMemory = 3, + /// Alignment requirements not met + AlignmentError = 4, + /// Destination slot already occupied + SlotOccupied = 5, + /// Invalid destination slot + InvalidSlot = 6, + /// Untyped has already been retyped (has children) + AlreadyRetyped = 7, + /// Object type requires specific size_bits + SizeMismatch = 8, + /// Maximum number of objects reached + ObjectLimitReached = 9, + /// Internal kernel error + InternalError = 10, +} + +impl RetypeError { + pub fn code(self) -> u32 { + self as u32 + } +} + +// ┌────────────────────────────────────────────────────────────┐ +// │ ALLOWED OPERATIONS ON UNTYPED │ +// ├────────────────────────────────────────────────────────────┤ +// │ ✓ Untyped_Retype → Create children (objects/sub-untypeds)│ +// │ ✓ CNode_Revoke → Delete all children, reset watermark │ +// │ ✓ CNode_Delete → Delete this cap (if no children) │ +// │ ✓ CNode_Move → Move cap to different slot │ +// ├────────────────────────────────────────────────────────────┤ +// │ DISALLOWED │ +// ├────────────────────────────────────────────────────────────┤ +// │ ✗ CNode_Copy → Cannot duplicate │ +// │ ✗ CNode_Mint → Cannot derive with reduced rights │ +// │ ✗ CNode_Mutate → Cannot modify │ +// └────────────────────────────────────────────────────────────┘ + +impl UntypedKey { + /// Retype untyped memory into a typed nucleus object. + /// + /// This is how ALL nucleus objects are created. + /// The untyped capability is consumed/reduced by the operation. + pub fn retype( + &self, + object_type: ObjectType, + size_bits: u8, // log2 of size (for variable-size objects) + dest_slot: CapSlot, + ) -> Result<(), RetypeError> { + let ret = unsafe { + libsyscall::protected_call3( + self.key.slot(), + UntypedOp::Retype, + object_type as u64, + dest_slot as u64, + size_bits as u64, + ) + }; + Error::from_code(ret) + } +} diff --git a/libs/syscall/src/lib.rs b/libs/syscall/src/lib.rs index 3e39ced39..f98f0a65e 100644 --- a/libs/syscall/src/lib.rs +++ b/libs/syscall/src/lib.rs @@ -1,63 +1,35 @@ #![no_std] -use core::result::Result; - // Syscall ABI: -// ┌─────────────────────────────────────────────────────────────────────────┐ -// │ CAPTBL COPY (cross-domain grant): │ -// │ ┌─────────────────────────────────────────────────────────────────┐ │ -// │ │ x0 = src_captbl x3 = dst_captbl │ │ -// │ │ x1 = COPY op x4 = dst_slot │ │ -// │ │ x2 = src_slot x5 = rights_mask ← derive+copy in one! │ │ -// │ └─────────────────────────────────────────────────────────────────┘ │ -// │ │ -// │ BUFFER MAP (with full control): │ -// │ ┌─────────────────────────────────────────────────────────────────┐ │ -// │ │ x0 = buffer_cap x3 = size │ │ -// │ │ x1 = MAP op x4 = offset ← map partial buffer │ │ -// │ │ x2 = virt_addr x5 = flags ← cache policy, etc. │ │ -// │ └─────────────────────────────────────────────────────────────────┘ │ -// │ │ -// │ ENDPOINT CALL (with inline payload): │ -// │ ┌─────────────────────────────────────────────────────────────────┐ │ -// │ │ x0 = endpoint_cap x3 = msg_word_1 │ │ -// │ │ x1 = CALL op x4 = msg_word_2 │ │ -// │ │ x2 = msg_word_0 x5 = msg_word_3 ← 4 words inline! │ │ -// │ └─────────────────────────────────────────────────────────────────┘ │ -// │ │ -// │ UNTYPED RETYPE (batch creation): │ -// │ ┌─────────────────────────────────────────────────────────────────┐ │ -// │ │ x0 = untyped_cap x3 = dest_captbl │ │ -// │ │ x1 = RETYPE op x4 = dest_slot_start │ │ -// │ │ x2 = obj_type x5 = count ← create N objects at once! │ │ -// │ └─────────────────────────────────────────────────────────────────┘ │ -// └─────────────────────────────────────────────────────────────────────────┘ - -pub type SyscallResult = Result<(u64, u64), SyscallError>; - -pub enum SyscallError { - PermissionDenied, - InvalidOp, - SlotOccupied, - AlreadyMapped, - NotMapped, - InvalidPointer, - Unknown, -} - -impl SyscallError { - pub fn from(val: u64) -> SyscallError { - match val { - 1 => SyscallError::PermissionDenied, - 2 => SyscallError::InvalidOp, - 3 => SyscallError::SlotOccupied, - 4 => SyscallError::AlreadyMapped, - 5 => SyscallError::NotMapped, - 6 => SyscallError::InvalidPointer, - _ => SyscallError::Unknown, - } - } -} +// ┌───────────────────────────────────────────────────────────────────────┐ +// │ CAPTBL COPY (cross-domain grant): │ +// │ ┌─────────────────────────────────────────────────────────────────┐ │ +// │ │ x0 = src_captbl x3 = dst_captbl │ │ +// │ │ x1 = COPY op x4 = dst_slot │ │ +// │ │ x2 = src_slot x5 = rights_mask ← derive+copy in one! │ │ +// │ └─────────────────────────────────────────────────────────────────┘ │ +// │ │ +// │ BUFFER MAP (with full control): │ +// │ ┌─────────────────────────────────────────────────────────────────┐ │ +// │ │ x0 = buffer_cap x3 = size │ │ +// │ │ x1 = MAP op x4 = offset ← map partial buffer │ │ +// │ │ x2 = virt_addr x5 = flags ← cache policy, etc. │ │ +// │ └─────────────────────────────────────────────────────────────────┘ │ +// │ │ +// │ ENDPOINT CALL (with inline payload): │ +// │ ┌─────────────────────────────────────────────────────────────────┐ │ +// │ │ x0 = endpoint_cap x3 = msg_word_1 │ │ +// │ │ x1 = CALL op x4 = msg_word_2 │ │ +// │ │ x2 = msg_word_0 x5 = msg_word_3 ← 4 words inline! │ │ +// │ └─────────────────────────────────────────────────────────────────┘ │ +// │ │ +// │ UNTYPED RETYPE (batch creation): │ +// │ ┌─────────────────────────────────────────────────────────────────┐ │ +// │ │ x0 = untyped_cap x3 = dest_captbl │ │ +// │ │ x1 = RETYPE op x4 = dest_slot_start │ │ +// │ │ x2 = obj_type x5 = count ← create N objects at once! │ │ +// │ └─────────────────────────────────────────────────────────────────┘ │ +// └───────────────────────────────────────────────────────────────────────┘ /// Single syscall ABI /// @@ -74,7 +46,7 @@ impl SyscallError { /// x1 = return value 0 /// x2 = return value 1 (if needed) #[inline(always)] -unsafe fn syscall6( +pub unsafe fn protected_call6( cap: u32, op: u32, a0: u64, @@ -104,8 +76,9 @@ unsafe fn syscall6( (r0, r1, r2) } +/// 5-arg invoke #[inline(always)] -pub unsafe fn protected_call6( +pub unsafe fn protected_call5( cap: u32, op: u32, a0: u64, @@ -113,89 +86,125 @@ pub unsafe fn protected_call6( a2: u64, a3: u64, a4: u64, - a5: u64, -) -> SyscallResult { - let (ret, val0, val1) = unsafe { syscall6(cap, op, a0, a1, a2, a3, a4, a5) }; - if ret == 0 { - return Ok((val0, val1)); - } else { - return Err(SyscallError::from(ret)); +) -> (u64, u64, u64) { + let r0: u64; + let r1: u64; + let r2: u64; + unsafe { + core::arch::asm!( + "svc #0", + inlateout("x0") cap as u64 => r0, + inlateout("x1") op as u64 => r1, + inlateout("x2") a0 => r2, + in("x3") a1, + in("x4") a2, + in("x5") a3, + in("x6") a4, + options(nostack), + ); } + (r0, r1, r2) } -// Most operations don't need all 6 args - provide convenience wrappers -// TODO: don't waste registers to zero them out for a less-than-6-args versions? Might be risky... - -/// 0-arg invoke (cap + op only) +/// 4-arg invoke #[inline(always)] -pub fn protected_call0(cap: u32, op: u32) -> SyscallResult { - let (ret, val0, val1) = unsafe { syscall6(cap, op, 0, 0, 0, 0, 0, 0) }; - if ret == 0 { - return Ok((val0, val1)); - } else { - return Err(SyscallError::from(ret)); +pub unsafe fn protected_call4( + cap: u32, + op: u32, + a0: u64, + a1: u64, + a2: u64, + a3: u64, +) -> (u64, u64, u64) { + let r0: u64; + let r1: u64; + let r2: u64; + unsafe { + core::arch::asm!( + "svc #0", + inlateout("x0") cap as u64 => r0, + inlateout("x1") op as u64 => r1, + inlateout("x2") a0 => r2, + in("x3") a1, + in("x4") a2, + in("x5") a3, + options(nostack), + ); } + (r0, r1, r2) } -/// 1-arg invoke +/// 3-arg invoke #[inline(always)] -pub fn protected_call1(cap: u32, op: u32, a0: u64) -> SyscallResult { - let (ret, val0, val1) = unsafe { syscall6(cap, op, a0, 0, 0, 0, 0, 0) }; - if ret == 0 { - return Ok((val0, val1)); - } else { - return Err(SyscallError::from(ret)); +pub unsafe fn protected_call3(cap: u32, op: u32, a0: u64, a1: u64, a2: u64) -> (u64, u64, u64) { + let r0: u64; + let r1: u64; + let r2: u64; + unsafe { + core::arch::asm!( + "svc #0", + inlateout("x0") cap as u64 => r0, + inlateout("x1") op as u64 => r1, + inlateout("x2") a0 => r2, + in("x3") a1, + in("x4") a2, + options(nostack), + ); } + (r0, r1, r2) } /// 2-arg invoke #[inline(always)] -pub fn protected_call2(cap: u32, op: u32, a0: u64, a1: u64) -> SyscallResult { - let (ret, val0, val1) = unsafe { syscall6(cap, op, a0, a1, 0, 0, 0, 0) }; - if ret == 0 { - return Ok((val0, val1)); - } else { - return Err(SyscallError::from(ret)); - } -} - -/// 3-arg invoke -#[inline(always)] -pub fn protected_call3(cap: u32, op: u32, a0: u64, a1: u64, a2: u64) -> SyscallResult { - let (ret, val0, val1) = unsafe { syscall6(cap, op, a0, a1, a2, 0, 0, 0) }; - if ret == 0 { - return Ok((val0, val1)); - } else { - return Err(SyscallError::from(ret)); +pub unsafe fn protected_call2(cap: u32, op: u32, a0: u64, a1: u64) -> (u64, u64, u64) { + let r0: u64; + let r1: u64; + let r2: u64; + unsafe { + core::arch::asm!( + "svc #0", + inlateout("x0") cap as u64 => r0, + inlateout("x1") op as u64 => r1, + inlateout("x2") a0 => r2, + in("x3") a1, + options(nostack), + ); } + (r0, r1, r2) } -/// 4-arg invoke +/// 1-arg invoke #[inline(always)] -pub fn protected_call4(cap: u32, op: u32, a0: u64, a1: u64, a2: u64, a3: u64) -> SyscallResult { - let (ret, val0, val1) = unsafe { syscall6(cap, op, a0, a1, a2, a3, 0, 0) }; - if ret == 0 { - return Ok((val0, val1)); - } else { - return Err(SyscallError::from(ret)); +pub unsafe fn protected_call1(cap: u32, op: u32, a0: u64) -> (u64, u64, u64) { + let r0: u64; + let r1: u64; + let r2: u64; + unsafe { + core::arch::asm!( + "svc #0", + inlateout("x0") cap as u64 => r0, + inlateout("x1") op as u64 => r1, + inlateout("x2") a0 => r2, + options(nostack), + ); } + (r0, r1, r2) } -/// 5-arg invoke +/// 0-arg invoke (cap + op only) #[inline(always)] -pub fn protected_call5( - cap: u32, - op: u32, - a0: u64, - a1: u64, - a2: u64, - a3: u64, - a4: u64, -) -> SyscallResult { - let (ret, val0, val1) = unsafe { syscall6(cap, op, a0, a1, a2, a3, a4, 0) }; - if ret == 0 { - return Ok((val0, val1)); - } else { - return Err(SyscallError::from(ret)); +pub unsafe fn protected_call0(cap: u32, op: u32) -> (u64, u64, u64) { + let r0: u64; + let r1: u64; + let r2: u64; + unsafe { + core::arch::asm!( + "svc #0", + inlateout("x0") cap as u64 => r0, + inlateout("x1") op as u64 => r1, + out("x2") r2, + options(nostack), + ); } + (r0, r1, r2) } From 9a06b774e9e922794ab37ff3c5331dc8e5c51039 Mon Sep 17 00:00:00 2001 From: Berkus Decker Date: Sun, 1 Feb 2026 08:51:10 +0200 Subject: [PATCH 056/107] wip: debug console trial --- kernel/init_thread/src/main.rs | 9 +++++++++ libs/object/src/debug_console.rs | 14 +++++++++++++- libs/object/src/key.rs | 8 ++++---- libs/object/src/key_table.rs | 1 + 4 files changed, 27 insertions(+), 5 deletions(-) diff --git a/kernel/init_thread/src/main.rs b/kernel/init_thread/src/main.rs index cd3cea6e9..069ab6a13 100644 --- a/kernel/init_thread/src/main.rs +++ b/kernel/init_thread/src/main.rs @@ -462,6 +462,15 @@ pub extern "C" fn init_thread_run(_dtb_ptr: *const u8) -> ! { // PHASE 8: Context switch to init domain // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + // We have domain caps here, can use: + let dbg = DebugConsoleKey::new(); + dbg.write( + "DEBCON| Debug output via capability invocation on domain's debug console capability", + ); + + let err = DebugConsoleKey::new_slot(KeySlot::NULL_CAP); + err.write("DEBCON| Invalid capability invocation - no output"); + // semi_println!("Switching to init domain..."); // semi_println!("═══════════════════════════════════════════════════════════"); diff --git a/libs/object/src/debug_console.rs b/libs/object/src/debug_console.rs index 3e6d06742..4e3e43f8b 100644 --- a/libs/object/src/debug_console.rs +++ b/libs/object/src/debug_console.rs @@ -1,4 +1,4 @@ -use crate::{CapError, Key}; +use crate::{CapError, Key, KeySlot}; // ================================================== // == Public user interface, usable from userspace == @@ -18,6 +18,18 @@ pub enum DebugConsoleOp { // Root domain gets a DebugConsoleCap, can delegate to others impl DebugConsoleKey { + pub const fn new() -> Self { + Self { + key: Key::new(KeySlot::DEBUG_CONSOLE), + } + } + + pub const fn new_slot(slot: KeySlot) -> Self { + Self { + key: Key::new(slot), + } + } + pub fn write(&self, s: &str) -> Result<(), CapError> { let (_ok, _, _) = unsafe { libsyscall::protected_call2( diff --git a/libs/object/src/key.rs b/libs/object/src/key.rs index 30302f9de..df934a8cd 100644 --- a/libs/object/src/key.rs +++ b/libs/object/src/key.rs @@ -1,4 +1,4 @@ -use core::marker::PhantomData; +use {crate::KeySlot, core::marker::PhantomData}; // ================================================== // == Public user interface, usable from userspace == @@ -8,12 +8,12 @@ use core::marker::PhantomData; #[derive(Copy, Clone, Debug, PartialEq, Eq)] #[repr(transparent)] pub struct Key { - slot: u32, // FIXME: KeySlot() + slot: KeySlot, _marker: PhantomData, } impl Key { - pub const fn new(slot: u32) -> Self { + pub const fn new(slot: KeySlot) -> Self { Self { slot, _marker: PhantomData, @@ -21,6 +21,6 @@ impl Key { } pub const fn slot(&self) -> u32 { - self.slot + self.slot.0 } } diff --git a/libs/object/src/key_table.rs b/libs/object/src/key_table.rs index 8e01fdf4f..b47161132 100644 --- a/libs/object/src/key_table.rs +++ b/libs/object/src/key_table.rs @@ -19,6 +19,7 @@ impl KeySlot { // CSpace layout with self-reference pub const CAPTBL_SELF: KeySlot = KeySlot(3); // Every domain has cap to own captbl here - or rather to KeyMaster // ... other well-known slots + pub const DEBUG_CONSOLE: KeySlot = KeySlot(127); // FIXME: randomly chosen for now } pub struct KeyTableKey { From b8f510347303f6dad1862360987fc1a0a33a4c61 Mon Sep 17 00:00:00 2001 From: Berkus Decker Date: Sun, 1 Feb 2026 10:15:58 +0200 Subject: [PATCH 057/107] fix: compile fixes #6 --- kernel/nucleus/src/api/debug_console.rs | 12 +++- kernel/nucleus/src/api/key_entry.rs | 12 ++-- kernel/nucleus/src/api/mod.rs | 10 +++- kernel/nucleus/src/main.rs | 30 ++++++---- .../src/objects/arch/aarch64_objects.rs | 31 ++++++---- kernel/nucleus/src/objects/arch/arch_pools.rs | 2 + kernel/nucleus/src/objects/arch/asid.rs | 7 +++ kernel/nucleus/src/objects/arch/asid_pool.rs | 13 +++++ kernel/nucleus/src/objects/arch/frame.rs | 8 ++- kernel/nucleus/src/objects/arch/mod.rs | 23 ++++++++ kernel/nucleus/src/objects/arch/page_table.rs | 12 ++++ kernel/nucleus/src/objects/arch/vspace.rs | 13 +++++ kernel/nucleus/src/objects/arch_objects.rs | 57 ++++++++++++++++--- kernel/nucleus/src/objects/debug_console.rs | 12 ++-- kernel/nucleus/src/objects/domain.rs | 4 +- kernel/nucleus/src/objects/mod.rs | 10 +++- kernel/nucleus/src/objects/nucleus.rs | 22 +++---- kernel/nucleus/src/objects/object_pool.rs | 2 + kernel/nucleus/src/objects/object_ref.rs | 10 ++-- libs/object/src/debug_console.rs | 11 ++++ libs/object/src/key_table.rs | 20 ++++--- libs/object/src/lib.rs | 15 +++-- libs/object/src/rights.rs | 13 +++-- 23 files changed, 265 insertions(+), 84 deletions(-) create mode 100644 kernel/nucleus/src/objects/arch/asid.rs create mode 100644 kernel/nucleus/src/objects/arch/asid_pool.rs create mode 100644 kernel/nucleus/src/objects/arch/vspace.rs diff --git a/kernel/nucleus/src/api/debug_console.rs b/kernel/nucleus/src/api/debug_console.rs index 3e52800a1..1417a02b0 100644 --- a/kernel/nucleus/src/api/debug_console.rs +++ b/kernel/nucleus/src/api/debug_console.rs @@ -1,4 +1,7 @@ -use libobject::{CapError, Key, debug_console::DebugConsoleOp}; +use { + crate::{api::KeyEntry, objects::DebugConsole}, + libobject::{CapError, Key, SyscallResult, debug_console::DebugConsoleOp}, +}; // ===================== // == Syscall handler == @@ -6,11 +9,14 @@ use libobject::{CapError, Key, debug_console::DebugConsoleOp}; #[inline] pub fn invoke(cap: &KeyEntry, op: u32, arg0: u64, arg1: u64) -> SyscallResult { - let console = cap.as_debug_console()?; + let console = cap.as_object_mut::()?; let op = DebugConsoleOp::try_from(op).map_err(|_| CapError::InvalidOperation)?; match op { - DebugConsoleOp::Write => console.handle_write(arg0, arg1), + // DebugConsoleOp::Write => console.handle_write(arg0, arg1), + DebugConsoleOp::Write => { + crate::objects::debug_console::DebugConsole::handle_write(arg0, arg1) + } _ => Err(CapError::InvalidOperation), } } diff --git a/kernel/nucleus/src/api/key_entry.rs b/kernel/nucleus/src/api/key_entry.rs index 2704f3a77..2a910a392 100644 --- a/kernel/nucleus/src/api/key_entry.rs +++ b/kernel/nucleus/src/api/key_entry.rs @@ -1,10 +1,12 @@ // ═══════════════════════════════════════════════════════════════════ // KEY ENTRY (CAPABILITY TABLE ENTRY) // ═══════════════════════════════════════════════════════════════════ +// FIXME: should be in objects? -use crate::{ - libobject::{KeySlot, ObjectType}, - objects::{NucleusObject, ObjectRef}, +use { + crate::objects::{NucleusObject, object_ref::ObjectRef}, + core::ptr::NonNull, + libobject::{CapError, KeySlot, ObjectType, Rights}, }; /// A single entry in a domain's capability table (KeyTable). @@ -49,7 +51,7 @@ impl KeyEntry { Self { object_ref: ObjectRef { ptr: NonNull::dangling(), - obj_type: ObjectType::Null, + obj_type: ObjectType::NULL, }, rights: Rights::empty(), parent_slot: 0xFFFF, @@ -77,7 +79,7 @@ impl KeyEntry { /// Check if this entry is valid (not null) #[inline] pub fn is_valid(&self) -> bool { - self.object_ref.obj_type != ObjectType::Null + self.object_ref.obj_type != ObjectType::NULL } /// Get the object type diff --git a/kernel/nucleus/src/api/mod.rs b/kernel/nucleus/src/api/mod.rs index 6f6b3b2d6..75776f9ad 100644 --- a/kernel/nucleus/src/api/mod.rs +++ b/kernel/nucleus/src/api/mod.rs @@ -1,5 +1,8 @@ use { - crate::objects::DebugConsole, + crate::{ + api::key_entry::KeyEntry, + objects::{ArchObjects, DebugConsole, Nucleus}, + }, libobject::{ArchType, CapError, CoreType, KeySlot, ObjectType}, }; @@ -62,7 +65,8 @@ fn core_invoke( // } CoreType::DebugConsole => { let debug_console = entry.as_object_mut::()?; - DebugConsole::invoke(debug_console, parampampam) + // DebugConsole::invoke(debug_console, entry.rights(), op, args, nucleus) + crate::api::debug_console::invoke(entry, op, args[0], args[1]) } // CoreType::Domain => { // let domain = entry.as_object_mut::()?; // api::domain::invoke(domain, entry.rights(), op, args) @@ -102,7 +106,7 @@ fn core_invoke( // let reply = entry.as_object_mut::()?; // api::reply::invoke(reply, op, args, nucleus) // } - _ => Err(CapError::UnsupportedCoreType), + _ => Err(CapError::UnsupportedCoreType(core_type)), } } diff --git a/kernel/nucleus/src/main.rs b/kernel/nucleus/src/main.rs index e21c1146b..2b01823a0 100644 --- a/kernel/nucleus/src/main.rs +++ b/kernel/nucleus/src/main.rs @@ -23,10 +23,7 @@ #![feature(core_intrinsics)] use { - crate::{ - api::{key_table::KeySlot, object_type::ArchType}, - nucleus::Nucleus, - }, + crate::objects::nucleus::Nucleus, cfg_if::cfg_if, core::{ arch::asm, @@ -35,11 +32,11 @@ use { time::Duration, }, libcpu::endless_sleep, - liblocking::IRQSafeNullLock, + liblocking::{IRQSafeNullLock, interface::Mutex}, liblog::{info, println, warn}, libmemory::mmu::AccessPermissions, + libobject::{ArchType, CapError, KeySlot}, libqemu::semi_println, - libsyscall::CapError, }; /// Syscall API - capability invocation handlers @@ -51,7 +48,11 @@ mod vectors; /// Global kernel state, protected by The Great Kernel Lock static mut NUCLEUS: IRQSafeNullLock>> = - IRQSafeNullLock::new(LazyCell::new(|| Nucleus:: {})); + IRQSafeNullLock::new(LazyCell::new(|| Nucleus:: { + current_domain: None, + dcb_pages: 0, + pools: objects::nucleus::NucleusPools { arch: 0 }, + })); #[panic_handler] fn panicked(info: &PanicInfo) -> ! { @@ -131,7 +132,12 @@ fn cap_invoke_handler( get_pc() ); - api::handle_cap_invoke(NUCLEUS.lock(), cap_slot, op, args) + let result = api::handle_cap_invoke( + NUCLEUS.lock(), + cap_slot, + op, + &[arg0, arg1, arg2, arg3, arg4, arg5], + ); // let cap = current_domain().keytable.lookup(cap_slot)?; // let args = &[arg0, arg1, arg2, arg3, arg4, arg5]; // FIXME temp @@ -148,10 +154,10 @@ fn cap_invoke_handler( // ObjectType::None => Err(SyscallError::InvalidSlot), // }; - // match result { - // Ok((v0, v1)) => (0, v0, v1), - // Err(e) => (e.code(), 0, 0), - // } + match result { + Ok((v0, v1)) => (0, v0, v1), + Err(e) => (e.code(), 0, 0), + } } fn get_pc() -> u64 { diff --git a/kernel/nucleus/src/objects/arch/aarch64_objects.rs b/kernel/nucleus/src/objects/arch/aarch64_objects.rs index 9901a866f..26fdd03ad 100644 --- a/kernel/nucleus/src/objects/arch/aarch64_objects.rs +++ b/kernel/nucleus/src/objects/arch/aarch64_objects.rs @@ -1,10 +1,18 @@ use { - crate::api::{ - arch::frame::AArch64Frame, - object_type::{ArchType, ObjectType}, + crate::{ + Nucleus, + objects::{ + ArchObjects, + arch::{ + AArch64ASID, AArch64ASIDPool, AArch64Frame, AArch64PageTable, AArch64VSpace, + ArchPools, + }, + arch_objects::FrameSize, + object_ref::ObjectRef, + }, }, libmemory::{phys_addr::PhysAddr, virt_addr::VirtAddr}, - libsyscall::CapError, + libobject::{ArchType, CapError, ObjectType, Rights}, }; // ═══════════════════════════════════════════════════════════════════ @@ -32,7 +40,7 @@ impl ArchObjects for AArch64 { 12 => Ok(4096), 21 => Ok(2 * 1024 * 1024), 30 => Ok(1024 * 1024 * 1024), - _ => Err(CapError::InvalidFrameSize(size_bits)), + _ => Err(CapError::InvalidFrameSize(size_bits as usize)), }, ArchType::PageTable => { if size_bits != 12 { @@ -101,9 +109,10 @@ impl ArchObjects for AArch64 { rights: Rights, op: u32, args: &[u64; 6], - kernel: &mut Kernel, + nucleus: &mut Nucleus, ) -> Result<(u64, u64), CapError> { - crate::api::arch::frame::invoke(frame, rights, op, args) + // crate::api::arch::frame::invoke(frame, rights, op, args) + Err(CapError::InvalidOperation) } // ───────────────────────────────────────────────────────────────── @@ -115,9 +124,10 @@ impl ArchObjects for AArch64 { rights: Rights, op: u32, args: &[u64; 6], - kernel: &mut Kernel, + nucleus: &mut Nucleus, ) -> Result<(u64, u64), CapError> { // crate::api::arch::page_table::invoke(pt, rights, op, args) + Err(CapError::InvalidOperation) } // ───────────────────────────────────────────────────────────────── @@ -129,9 +139,10 @@ impl ArchObjects for AArch64 { rights: Rights, op: u32, args: &[u64; 6], - kernel: &mut Kernel, + nucleus: &mut Nucleus, ) -> Result<(u64, u64), CapError> { // crate::api::arch::vspace::invoke(vspace, rights, op, args) + Err(CapError::InvalidOperation) } // ───────────────────────────────────────────────────────────────── @@ -143,7 +154,7 @@ impl ArchObjects for AArch64 { rights: Rights, op: u32, args: &[u64; 6], - _kernel: &mut Kernel, + _nucleus: &mut Nucleus, ) -> Result<(u64, u64), CapError> { // Most ASID operations go through VSpace.AssignASID // Direct pool operations are rare diff --git a/kernel/nucleus/src/objects/arch/arch_pools.rs b/kernel/nucleus/src/objects/arch/arch_pools.rs index 3f7017fae..95e785f29 100644 --- a/kernel/nucleus/src/objects/arch/arch_pools.rs +++ b/kernel/nucleus/src/objects/arch/arch_pools.rs @@ -1,3 +1,5 @@ +use crate::objects::{ArchObjects, ObjectPool}; + // ═══════════════════════════════════════════════════════════════════ // ARCHITECTURE-SPECIFIC OBJECT POOLS // ═══════════════════════════════════════════════════════════════════ diff --git a/kernel/nucleus/src/objects/arch/asid.rs b/kernel/nucleus/src/objects/arch/asid.rs new file mode 100644 index 000000000..51980d6ce --- /dev/null +++ b/kernel/nucleus/src/objects/arch/asid.rs @@ -0,0 +1,7 @@ +use {crate::objects::NucleusObject, libobject::ObjectType}; + +pub struct AArch64ASID; + +impl NucleusObject for AArch64ASID { + const TYPE: ObjectType = ObjectType::ASID; +} diff --git a/kernel/nucleus/src/objects/arch/asid_pool.rs b/kernel/nucleus/src/objects/arch/asid_pool.rs new file mode 100644 index 000000000..5bcb9b482 --- /dev/null +++ b/kernel/nucleus/src/objects/arch/asid_pool.rs @@ -0,0 +1,13 @@ +use {crate::objects::NucleusObject, libobject::ObjectType}; + +pub struct AArch64ASIDPool; + +impl AArch64ASIDPool { + pub fn new() -> Self { + Self + } +} + +impl NucleusObject for AArch64ASIDPool { + const TYPE: ObjectType = ObjectType::ASID_POOL; +} diff --git a/kernel/nucleus/src/objects/arch/frame.rs b/kernel/nucleus/src/objects/arch/frame.rs index 50b8d7a3a..b6fdc98fa 100644 --- a/kernel/nucleus/src/objects/arch/frame.rs +++ b/kernel/nucleus/src/objects/arch/frame.rs @@ -2,7 +2,11 @@ // AArch64 Frame (Physical Page) // ───────────────────────────────────────────────────────────────── -use {crate::api::key_table::KeySlot, libmemory::virt_addr::VirtAddr, libsyscall::CapError}; +use { + crate::objects::{NucleusObject, arch_objects::FrameSize}, + libmemory::{phys_addr::PhysAddr, virt_addr::VirtAddr}, + libobject::{CapError, KeySlot, ObjectType}, +}; /// A physical memory frame on AArch64. /// @@ -46,5 +50,5 @@ impl AArch64Frame { } impl NucleusObject for AArch64Frame { - const TYPE: ObjectType = ObjectType::Frame; + const TYPE: ObjectType = ObjectType::FRAME; } diff --git a/kernel/nucleus/src/objects/arch/mod.rs b/kernel/nucleus/src/objects/arch/mod.rs index 52bbb603f..16a1afdde 100644 --- a/kernel/nucleus/src/objects/arch/mod.rs +++ b/kernel/nucleus/src/objects/arch/mod.rs @@ -3,7 +3,30 @@ pub mod aarch64_objects; #[cfg(target_arch = "aarch64")] pub use aarch64_objects::AArch64 as ArchObjectsImpl; +pub mod arch_pools; +pub use arch_pools::ArchPools; + +#[cfg(target_arch = "aarch64")] +pub mod asid; +#[cfg(target_arch = "aarch64")] +pub use asid::AArch64ASID; + +#[cfg(target_arch = "aarch64")] +pub mod asid_pool; +#[cfg(target_arch = "aarch64")] +pub use asid_pool::AArch64ASIDPool; + #[cfg(target_arch = "aarch64")] pub mod frame; #[cfg(target_arch = "aarch64")] pub use frame::AArch64Frame; + +#[cfg(target_arch = "aarch64")] +pub mod page_table; +#[cfg(target_arch = "aarch64")] +pub use page_table::AArch64PageTable; + +#[cfg(target_arch = "aarch64")] +pub mod vspace; +#[cfg(target_arch = "aarch64")] +pub use vspace::AArch64VSpace; diff --git a/kernel/nucleus/src/objects/arch/page_table.rs b/kernel/nucleus/src/objects/arch/page_table.rs index e2fe3c313..64929bd29 100644 --- a/kernel/nucleus/src/objects/arch/page_table.rs +++ b/kernel/nucleus/src/objects/arch/page_table.rs @@ -1 +1,13 @@ +use {crate::objects::NucleusObject, libmemory::phys_addr::PhysAddr, libobject::ObjectType}; + pub struct AArch64PageTable; + +impl AArch64PageTable { + pub fn new(_addr: PhysAddr) -> Self { + Self + } +} + +impl NucleusObject for AArch64PageTable { + const TYPE: ObjectType = ObjectType::PAGE_TABLE; +} diff --git a/kernel/nucleus/src/objects/arch/vspace.rs b/kernel/nucleus/src/objects/arch/vspace.rs new file mode 100644 index 000000000..5de5e290f --- /dev/null +++ b/kernel/nucleus/src/objects/arch/vspace.rs @@ -0,0 +1,13 @@ +use {crate::objects::NucleusObject, libobject::ObjectType}; + +pub struct AArch64VSpace; + +impl AArch64VSpace { + pub fn new() -> Self { + Self + } +} + +impl NucleusObject for AArch64VSpace { + const TYPE: ObjectType = ObjectType::VSPACE; +} diff --git a/kernel/nucleus/src/objects/arch_objects.rs b/kernel/nucleus/src/objects/arch_objects.rs index ab9550cff..50369a250 100644 --- a/kernel/nucleus/src/objects/arch_objects.rs +++ b/kernel/nucleus/src/objects/arch_objects.rs @@ -1,9 +1,50 @@ -use {crate::objects::NucleusObject, libobject::ArchType, libsyscall::CapError}; +use { + crate::{ + api::key_entry::KeyEntry, + objects::{NucleusObject, arch::ArchPools, nucleus::Nucleus, object_ref::ObjectRef}, + }, + libmemory::phys_addr::PhysAddr, + libobject::{ArchType, CapError, Rights}, +}; // ═══════════════════════════════════════════════════════════════════ // ARCH OBJECTS TRAIT WITH INVOKE METHODS // ═══════════════════════════════════════════════════════════════════ +/// Frame size enumeration (common across architectures) +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub enum FrameSize { + /// 4KB (standard page) + Small, // 12 bits + /// 2MB (large page / section) + Large, // 21 bits + /// 1GB (huge page / supersection) + Huge, // 30 bits +} + +impl FrameSize { + pub const fn bits(&self) -> u8 { + match self { + FrameSize::Small => 12, + FrameSize::Large => 21, + FrameSize::Huge => 30, + } + } + + pub fn from_bits(bits: usize) -> Result { + match bits { + 12 => Ok(FrameSize::Small), + 21 => Ok(FrameSize::Large), + 30 => Ok(FrameSize::Huge), + _ => Err(()), + } + } + + pub const fn size(&self) -> usize { + 1 << self.bits() + } +} + /// Architecture abstraction trait - extended with invoke methods pub trait ArchObjects: Sized + 'static { // ─── Associated Types ─── @@ -35,7 +76,7 @@ pub trait ArchObjects: Sized + 'static { rights: Rights, op: u32, args: &[u64; 6], - kernel: &mut Kernel, + nucleus: &mut Nucleus, ) -> Result<(u64, u64), CapError>; fn invoke_page_table( @@ -43,7 +84,7 @@ pub trait ArchObjects: Sized + 'static { rights: Rights, op: u32, args: &[u64; 6], - kernel: &mut Kernel, + nucleus: &mut Nucleus, ) -> Result<(u64, u64), CapError>; fn invoke_vspace( @@ -51,7 +92,7 @@ pub trait ArchObjects: Sized + 'static { rights: Rights, op: u32, args: &[u64; 6], - kernel: &mut Kernel, + nucleus: &mut Nucleus, ) -> Result<(u64, u64), CapError>; fn invoke_asid_pool( @@ -59,7 +100,7 @@ pub trait ArchObjects: Sized + 'static { rights: Rights, op: u32, args: &[u64; 6], - kernel: &mut Kernel, + nucleus: &mut Nucleus, ) -> Result<(u64, u64), CapError>; fn invoke_asid( @@ -74,7 +115,7 @@ pub trait ArchObjects: Sized + 'static { _entry: &mut KeyEntry, _op: u32, _args: &[u64; 6], - _kernel: &mut Kernel, + _nucleus: &mut Nucleus, ) -> Result<(u64, u64), CapError> { Err(CapError::UnsupportedArchType(ArchType::IOSpace)) } @@ -83,7 +124,7 @@ pub trait ArchObjects: Sized + 'static { _entry: &mut KeyEntry, _op: u32, _args: &[u64; 6], - _kernel: &mut Kernel, + _nucleus: &mut Nucleus, ) -> Result<(u64, u64), CapError> { Err(CapError::UnsupportedArchType(ArchType::IRQHandler)) } @@ -92,7 +133,7 @@ pub trait ArchObjects: Sized + 'static { _entry: &mut KeyEntry, _op: u32, _args: &[u64; 6], - _kernel: &mut Kernel, + _nucleus: &mut Nucleus, ) -> Result<(u64, u64), CapError> { Err(CapError::UnsupportedArchType(ArchType::IRQControl)) } diff --git a/kernel/nucleus/src/objects/debug_console.rs b/kernel/nucleus/src/objects/debug_console.rs index d79617bb6..2d8c681f9 100644 --- a/kernel/nucleus/src/objects/debug_console.rs +++ b/kernel/nucleus/src/objects/debug_console.rs @@ -1,20 +1,24 @@ -use {core::slice, libobject::ObjectType}; +use { + crate::objects::NucleusObject, + core::slice, + libobject::{CapError, ObjectType}, +}; // ==================== // == Nucleus object == // ==================== -struct DebugConsole; +pub struct DebugConsole; impl DebugConsole { - fn handle_write(ptr: u64, len: u64) -> Result<(), SyscallError> { + fn handle_write(ptr: u64, len: u64) -> Result<(), CapError> { let slice = unsafe { slice::from_raw_parts(ptr as *const u8, len as usize) }; let mut buf = [0u8; 4096]; // SAFETY: Need to validate user pointer is valid, need to copy via kernel physmem mapping. buf.copy_from_slice(slice); buf[slice.len()] = 0; let cstr = unsafe { core::ffi::CStr::from_bytes_with_nul(&buf[..=slice.len() + 1]) } - .map_err(SyscallError::Unknown)?; + .map_err(CapError::Unknown)?; libqemu::semihosting::sys_write0_call(cstr); Ok(()) } diff --git a/kernel/nucleus/src/objects/domain.rs b/kernel/nucleus/src/objects/domain.rs index 660b799b8..b1f5d42f3 100644 --- a/kernel/nucleus/src/objects/domain.rs +++ b/kernel/nucleus/src/objects/domain.rs @@ -156,7 +156,7 @@ impl DcbView { } // Domain scheduling support in kernel: -impl Nucleus { +impl Domain { /// Called when domain is activated (receives CPU time) fn activate_domain(&mut self, id: DomainId, time_budget: u64) { let dcb = self.dcb_mut(id); @@ -241,5 +241,5 @@ impl Nucleus { // - Then can safely read other fields with Relaxed impl NucleusObject for Domain { - const TYPE: ObjectType = ObjectType::Domain; + const TYPE: ObjectType = ObjectType::DOMAIN; } diff --git a/kernel/nucleus/src/objects/mod.rs b/kernel/nucleus/src/objects/mod.rs index 1391da047..274485a2e 100644 --- a/kernel/nucleus/src/objects/mod.rs +++ b/kernel/nucleus/src/objects/mod.rs @@ -1,6 +1,12 @@ -// pub mod arch; +pub mod arch; pub mod arch_objects; +pub mod debug_console; +pub mod nucleus; pub mod nucleus_object; +pub mod object_pool; pub mod object_ref; -pub use {arch::ArchObjectsImpl, arch_objects::ArchObjects, nucleus_object::NucleusObject}; +pub use { + arch::ArchObjectsImpl, arch_objects::ArchObjects, debug_console::DebugConsole, + nucleus::Nucleus, nucleus_object::NucleusObject, object_pool::ObjectPool, +}; diff --git a/kernel/nucleus/src/objects/nucleus.rs b/kernel/nucleus/src/objects/nucleus.rs index 527cc205c..8cb534963 100644 --- a/kernel/nucleus/src/objects/nucleus.rs +++ b/kernel/nucleus/src/objects/nucleus.rs @@ -1,3 +1,5 @@ +use crate::objects::{ArchObjects, arch::ArchPools}; + // ┌─────────────────────────────────────────────────────────────────────┐ // │ KERNEL TYPE STRUCTURE │ // ├─────────────────────────────────────────────────────────────────────┤ @@ -34,15 +36,15 @@ /// All kernel object pools - both core and architecture-specific pub struct NucleusPools { // ─── Core Object Pools ─── - pub untypeds: ObjectPool, - pub domains: ObjectPool, - pub keytables: ObjectPool, - pub notifications: ObjectPool, - pub event_counts: ObjectPool, - pub endpoints: ObjectPool, - pub time_slices: ObjectPool, - pub buffers: ObjectPool, - pub replies: ObjectPool, + // pub untypeds: ObjectPool, + // pub domains: ObjectPool, + // pub keytables: ObjectPool, + // pub notifications: ObjectPool, + // pub event_counts: ObjectPool, + // pub endpoints: ObjectPool, + // pub time_slices: ObjectPool, + // pub buffers: ObjectPool, + // pub replies: ObjectPool, // ─── Architecture-Specific Pools ─── pub arch: ArchPools, @@ -53,7 +55,7 @@ pub struct Nucleus { /// All object pools pub pools: NucleusPools, /// Currently running domain - pub current_domain: Option, // FIXME: not option, always something (Idle or other) + pub current_domain: Option, // FIXME: not option, always something (Idle or other) /// DCB shared pages pub dcb_pages: DcbPages, } diff --git a/kernel/nucleus/src/objects/object_pool.rs b/kernel/nucleus/src/objects/object_pool.rs index 3dde66489..68bccb3b2 100644 --- a/kernel/nucleus/src/objects/object_pool.rs +++ b/kernel/nucleus/src/objects/object_pool.rs @@ -1,3 +1,5 @@ +use crate::objects::NucleusObject; + // ═══════════════════════════════════════════════════════════════════ // OBJECT POOLS // ═══════════════════════════════════════════════════════════════════ diff --git a/kernel/nucleus/src/objects/object_ref.rs b/kernel/nucleus/src/objects/object_ref.rs index 60a54e547..166986dcc 100644 --- a/kernel/nucleus/src/objects/object_ref.rs +++ b/kernel/nucleus/src/objects/object_ref.rs @@ -1,6 +1,7 @@ use { - super::NucleusObject, crate::api::object_type::ObjectType, core::ptr::NonNull, - libsyscall::CapError, + super::NucleusObject, + core::ptr::NonNull, + libobject::{CapError, object_type::ObjectType}, }; // ═══════════════════════════════════════════════════════════════════ @@ -32,7 +33,7 @@ impl ObjectRef { /// Caller must ensure the pointer is valid and properly aligned pub unsafe fn from_raw(ptr: *mut T) -> Self { Self { - ptr: NonNull::new_unchecked(ptr.cast()), + ptr: unsafe { NonNull::new_unchecked(ptr.cast()) }, obj_type: T::TYPE, } } @@ -77,9 +78,10 @@ impl ObjectRef { /// Cast with error on type mismatch (mutable) #[inline] pub fn as_type_mut(&mut self) -> Result<&mut T, CapError> { + let found = self.obj_type; self.try_as_mut().ok_or(CapError::TypeMismatch { expected: T::TYPE, - found: self.obj_type, + found, }) } } diff --git a/libs/object/src/debug_console.rs b/libs/object/src/debug_console.rs index 4e3e43f8b..df9a62251 100644 --- a/libs/object/src/debug_console.rs +++ b/libs/object/src/debug_console.rs @@ -16,6 +16,17 @@ pub enum DebugConsoleOp { Write = 0, } +impl TryFrom for DebugConsoleOp { + type Error = CapError; + + fn try_from(value: u32) -> Result { + match value { + 0 => Ok(DebugConsoleOp::Write), + _ => Err(CapError::InvalidOperation), + } + } +} + // Root domain gets a DebugConsoleCap, can delegate to others impl DebugConsoleKey { pub const fn new() -> Self { diff --git a/libs/object/src/key_table.rs b/libs/object/src/key_table.rs index b47161132..7a9951a21 100644 --- a/libs/object/src/key_table.rs +++ b/libs/object/src/key_table.rs @@ -51,17 +51,20 @@ impl KeyTableKey { dst_slot: u32, rights: Rights, ) -> Result<(), CapError> { - let (_ok, _, _) = unsafe { + let (ok, _, _) = unsafe { protected_call4( self.key.slot(), KeyTableOp::CopyDerive as u32, src_slot as u64, dst_captbl.key.slot() as u64, dst_slot as u64, - rights.bits(), + rights.bits() as u64, ) }; - Ok(()) + match ok { + 0 => Ok(()), + _ => Err(CapError::Unknown), + } } // fn activate(&self, slot: u32, object: NucleusObject) -> Result<()> { @@ -78,9 +81,10 @@ impl KeyTableKey { // Ok(()) // } - fn r#move() {} + /// Move the key, named "transfer" to avoid clashing with Rust's reserved word. + pub fn transfer() {} - fn delete(&mut self, slot: u32) -> Result<(), CapError> { + pub fn delete(&mut self, slot: u32) -> Result<(), CapError> { // TODO: Must invoke on self-captbl cap let (_ok, _, _) = unsafe { protected_call1(self.key.slot(), KeyTableOp::Delete as u32, slot as u64) }; @@ -88,14 +92,14 @@ impl KeyTableKey { } // Revoke all children of cap in slot - fn revoke(&self, _captbl: &KeyTableKey, slot: u32) -> Result<(), CapError> { + pub fn revoke(&self, _captbl: &KeyTableKey, slot: u32) -> Result<(), CapError> { let (_ok, _, _) = unsafe { protected_call1(self.key.slot(), KeyTableOp::Revoke as u32, slot as u64) }; Ok(()) } // User code to copy cap to another domain (if you have their captbl cap): - fn grant_to( + pub fn grant_to( &self, my_slot: u32, their_captbl: &KeyTableKey, @@ -108,7 +112,7 @@ impl KeyTableKey { my_slot as u64, their_captbl.key.slot() as u64, their_slot as u64, - same_rights as u64, + Rights::all().bits() as u64, ) }; Ok(()) diff --git a/libs/object/src/lib.rs b/libs/object/src/lib.rs index 102a2c49a..7c8e6c9a3 100644 --- a/libs/object/src/lib.rs +++ b/libs/object/src/lib.rs @@ -17,6 +17,7 @@ pub type SyscallResult = core::result::Result<(u64, u64), CapError>; pub enum CapError { Unknown, + NullCapability, InvalidPointer, InsufficientRights, NotMapped, @@ -27,19 +28,21 @@ pub enum CapError { InvalidSlot(KeySlot), EmptySlot(KeySlot), SlotOccupied(KeySlot), + // Key types NotCoreType(ObjectType), UnknownCoreType(u8), + UnsupportedCoreType(CoreType), NotArchType(ObjectType), UnknownArchType(u8), - UnsupportedArchType(u8), - InsufficientMemory, - PoolExhausted, + UnsupportedArchType(ArchType), InvalidObjectType(ObjectType), - InvalidSize(usize), - NullCapability, - InvalidFrameSize(usize), TypeMismatch { expected: ObjectType, found: ObjectType, }, + // Object pools + InsufficientMemory, + PoolExhausted, + InvalidSize(usize), + InvalidFrameSize(usize), } diff --git a/libs/object/src/rights.rs b/libs/object/src/rights.rs index 0aa7fc2b9..e952a4bd3 100644 --- a/libs/object/src/rights.rs +++ b/libs/object/src/rights.rs @@ -1,4 +1,4 @@ -pub type Rights = u8; +pub struct Rights(pub u8); impl Rights { pub const READ: u8 = 0x1; @@ -9,10 +9,13 @@ impl Rights { pub const CALL: u8 = 0x4; pub const GRANT: u8 = 0x8; - pub fn empty() -> Rights { - 0 + pub const fn empty() -> Rights { + Rights(0) } - pub fn all() -> Rights { - 0xF + pub const fn all() -> Rights { + Rights(0xF) + } + pub fn bits(&self) -> u8 { + self.0 } } From c43eae7a554ab66f38d0a7282a6c1889af3aba34 Mon Sep 17 00:00:00 2001 From: Berkus Decker Date: Sun, 1 Feb 2026 16:34:09 +0200 Subject: [PATCH 058/107] wip: Domain pages and DCB user-space view --- kernel/nucleus/src/objects/domain.rs | 401 +++++++++++----------- kernel/nucleus/src/objects/mod.rs | 1 + kernel/nucleus/src/objects/nucleus.rs | 131 +++++++- libs/memory/src/virt_addr.rs | 21 +- libs/object/Cargo.toml | 1 + libs/object/src/domain.rs | 465 +++++++++++++++++++++++--- libs/object/src/lib.rs | 1 + 7 files changed, 770 insertions(+), 251 deletions(-) diff --git a/kernel/nucleus/src/objects/domain.rs b/kernel/nucleus/src/objects/domain.rs index b1f5d42f3..7f88d4ad9 100644 --- a/kernel/nucleus/src/objects/domain.rs +++ b/kernel/nucleus/src/objects/domain.rs @@ -1,112 +1,20 @@ -use core::sync::atomic::Ordering; +use { + crate::objects::NucleusObject, + core::{ptr::NonNull, sync::atomic::Ordering}, + libmemory::{phys_addr::PhysAddr, virt_addr::VirtAddr}, + libobject::{ + ObjectType, + domain::{DcbPage, DomainControlBlock, DomainId, DomainState}, + }, +}; // ==================== // == Nucleus object == // ==================== -struct Domain; - -impl Domain { - // Initialize new domain's cspace - fn init_cspace(&mut self) { - // Slot 0: capability to this captbl itself - self.cspace[CAPTBL_SELF] = Cap::new(ObjectType::KeyTable, self.cspace_id); - // Now domain can manipulate its own caps - } -} - -#[repr(u32)] -#[derive(Copy, Clone, Debug, PartialEq, Eq)] -pub enum DomainState { - /// Just created, never run - Inactive = 0, - /// Ready to receive CPU time - Runnable = 1, - /// Currently executing (only one domain per CPU) - Running = 2, - /// Waiting on notification/event/endpoint - Blocked = 3, - /// Explicitly suspended by parent - Suspended = 4, - /// Faulted, needs handler - Faulted = 5, -} - -#[repr(u32)] -#[derive(Copy, Clone, Debug)] -pub enum BlockReason { - None = 0, - Notification = 1, // NotifyCap::wait() - EventCount = 2, // EventCountCap::await_ge() - Endpoint = 3, // EndpointCap::call() or recv() - TimeDonated = 4, // Donated time, waiting for return -} - -// pub enum DeactivateReason { -// TimeExhausted, -// BlockedOnEvent(EndpointCap), <-- BlockReason::Endpoint -// Yielded, -// Faulted(fault), -// } - -/// Domain Control Block -/// Our DCB structure - combining Nemesis ideas with our capability model -/// -/// Key insight: split into kernel-private and shared sections -#[repr(C, align(128))] // Cache-line aligned -pub struct DomainControlBlock { - // ═══════════════════════════════════════════════════════════ - // SHARED SECTION (read-only mapped to userspace) - // ═══════════════════════════════════════════════════════════ - // ─── Identity ─── - pub id: DomainId, - pub name: [u8; 24], - - // ─── Execution State (Acquire/Release on state field) ─── - pub state: AtomicU32, // DomainState - pub block_reason: AtomicU32, // BlockReason (if blocked) - pub blocked_on: AtomicU32, // Cap slot we're blocked on - - // ─── Time Accounting (QoS) ─── - /// Cumulative CPU time consumed (nanoseconds) - pub time_used_ns: AtomicU64, - /// Time remaining in current activation - pub time_remaining_ns: AtomicU64, - /// Number of times activated - pub activation_count: AtomicU64, - - // ─── Event State ─── - pub pending_notifications: AtomicU64, // Bitmap of pending notify caps - /// Number of pending events (sum across all endpoints) - pub pending_events: AtomicU32, // Number of event counts with data - - /// Endpoint that caused last wakeup - pub last_event_ep: AtomicU32, - - // ─── Scheduling Parameters ─── - /// Parent scheduler domain - pub scheduler: DomainId, - /// Scheduling priority/parameters - pub priority: u32, - /// Allocation period (for periodic domains) - pub period_ns: u64, - /// CPU allocation per period - pub budget_ns: u64, // slice_ns - - /// Scheduled deadline (absolute time) - pub deadline: AtomicU64, - - // ─── Fault Information ─── - /// Last fault type (if any) - pub fault_type: AtomicU32, - - /// Fault address - pub fault_addr: AtomicU64, - - pub fault_cap: AtomicU32, // Cap slot that caused fault - - // Padding to cache line - _pad: [u8; 16], +/// This is a nucleus-visible half of domain structure. +/// The DomainControlBlock is user-visible and is defined in libobject. +struct Domain { // ═══════════════════════════════════════════════════════════ // PRIVATE SECTION (kernel only, NOT mapped to userspace) // ═══════════════════════════════════════════════════════════ @@ -119,127 +27,222 @@ pub struct DomainControlBlock { } // Verify size for cache alignment -const _: () = assert!(core::mem::size_of::() == 128); +const _: () = assert!(core::mem::size_of::() == 4096); -// 32 DCBs per 4KB page -// Multiple pages for more domains +impl NucleusObject for Domain { + const TYPE: ObjectType = ObjectType::DOMAIN; +} -// Userspace sees: const DCB_BASE: *const DomainControlBlock = 0xFFFF_0000_0000_0000; // FIXME: pervasives -// Access DCB n: &*DCB_BASE.add(n) +impl Domain { + // Initialize new domain's cspace + // fn init_cspace(&mut self) { + // // Slot 0: capability to this captbl itself + // self.cspace[CAPTBL_SELF] = Cap::new(ObjectType::KeyTable, self.cspace_id); + // // Now domain can manipulate its own caps + // } +} -/// Userspace view of DCB array -/// Mapped read-only at a well-known address -pub struct DcbView { - base: *const DomainControlBlock, +// ## Memory Ordering Considerations +// +// KERNEL (writer) USERSPACE (reader) +// ─────────────── ────────────────── +// +// // Update multiple fields +// dcb.time_used.store(x, Relaxed); +// dcb.time_remaining.store(y, Relaxed); +// dcb.state.store(z, Release); ──────────────────────┐ +// │ │ +// │ Release ensures all │ +// │ prior writes visible │ +// ▼ ▼ +// let state = dcb.state.load(Acquire); +// // Acquire ensures we see +// // all writes before the Release +// let used = dcb.time_used.load(Relaxed); +// let rem = dcb.time_remaining.load(Relaxed); +// +// Protocol: +// - Kernel does Release store on state LAST +// - Userspace does Acquire load on state FIRST +// - Then can safely read other fields with Relaxed + +// ═══════════════════════════════════════════════════════════════════ +// DCB PAGES MANAGER +// ═══════════════════════════════════════════════════════════════════ + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum DcbError { + TooManyPages, + PageNotAllocated, + NoFreeDomains, + NotAllocated, + InvalidDomainId, +} + +/// Manager for all DCB pages in the system. +/// +/// Provides: +/// - Kernel-side mutable access for state updates +/// - Physical addresses for user-space mapping +/// - Domain ID allocation +pub struct DcbPages { + /// Array of DCB pages (kernel virtual addresses) + pages: [Option<&'static mut DcbPage>; Self::MAX_PAGES], + /// Physical addresses of each page (for user mapping) + phys_addrs: [Option>; Self::MAX_PAGES], + /// Number of allocated pages + num_pages: usize, + /// Next domain ID to allocate + next_domain_id: u32, + /// Bitmap of allocated domain IDs + allocated: [u64; Self::MAX_DOMAINS / 64], } -impl DcbView { - /// Get from well-known address (set up by kernel at domain creation) +impl DcbPages { + /// Maximum number of DCB pages (supports up to 8192 domains) + pub const MAX_PAGES: usize = 256; + /// Maximum domains (256 pages × 32 DCBs/page) + pub const MAX_DOMAINS: usize = Self::MAX_PAGES * DcbPage::DCBS_PER_PAGE as usize; + + /// Well-known user-space base address for DCB mapping + /// This is mapped read-only into all domains + pub const USER_BASE: VirtAddr = VirtAddr::new(0x0000_7FFF_FE00_0000); + + /// Create empty DCB pages manager pub const fn new() -> Self { Self { - base: 0xFFFF_0000_0000_0000 as *const DomainControlBlock, + pages: [const { None }; Self::MAX_PAGES], + phys_addrs: [const { None }; Self::MAX_PAGES], + num_pages: 0, + next_domain_id: 0, + allocated: [0; Self::MAX_DOMAINS / 64], } } - /// Read any domain's state - #[inline(always)] - pub fn get(&self, id: DomainId) -> &DomainControlBlock { - unsafe { &*self.base.add(id.0 as usize) } - } + /// Add a new DCB page (called during kernel init) + /// + /// # Safety + /// - `page` must be valid, aligned, and not aliased + /// - `phys_addr` must be the correct physical address + pub unsafe fn add_page( + &mut self, + page: *mut DcbPage, + phys_addr: PhysAddr, + ) -> Result { + if self.num_pages >= Self::MAX_PAGES { + return Err(DcbError::TooManyPages); + } - /// Get my own DCB - #[inline(always)] - pub fn myself(&self) -> &DomainControlBlock { - // Current domain ID stored in thread-local or well-known register - self.get(current_domain_id()) - } -} + let idx = self.num_pages; + self.pages[idx] = Some(&mut *page); + self.phys_addrs[idx] = Some(phys_addr); + self.num_pages += 1; -// Domain scheduling support in kernel: -impl Domain { - /// Called when domain is activated (receives CPU time) - fn activate_domain(&mut self, id: DomainId, time_budget: u64) { - let dcb = self.dcb_mut(id); - - dcb.state - .store(DomainState::Running as u32, Ordering::Release); - dcb.time_remaining_ns.store(time_budget, Ordering::Release); - dcb.deadline.store(now() + time_budget, Ordering::Release); + Ok(idx) } - /// Called on every context switch FROM this domain - fn deactivate_domain(&mut self, id: DomainId, reason: DeactivateReason) { - let dcb = self.dcb_mut(id); - let elapsed = /* calculate from timer */0; + /// Allocate a new domain ID and initialize its DCB + pub fn allocate_domain(&mut self, scheduler_id: DomainId) -> Result { + // Find free slot + let id = self.find_free_slot()?; - // Update time accounting - dcb.time_used_ns.fetch_add(elapsed, Ordering::Relaxed); - dcb.time_remaining_ns.fetch_sub(elapsed, Ordering::Relaxed); + // Mark as allocated + let word = id as usize / 64; + let bit = id as usize % 64; + self.allocated[word] |= 1 << bit; - // Update state - match reason { - DeactivateReason::TimeExhausted => { - dcb.state - .store(DomainState::Runnable as u32, Ordering::Release); - } - DeactivateReason::BlockedOnEvent(ep) => { - dcb.state - .store(DomainState::Blocked as u32, Ordering::Release); - dcb.block_reason - .store(BlockReason::Event as u32, Ordering::Release); - dcb.last_event_ep.store(ep, Ordering::Release); - } - DeactivateReason::Yielded => { - dcb.state - .store(DomainState::Runnable as u32, Ordering::Release); - } - DeactivateReason::Faulted(fault) => { - dcb.state - .store(DomainState::Faulted as u32, Ordering::Release); - dcb.fault_type.store(fault.type_code(), Ordering::Release); - dcb.fault_addr.store(fault.addr(), Ordering::Release); - } - } + // Initialize DCB + let domain_id = DomainId(id); + let dcb = self.get_mut(domain_id).ok_or(DcbError::PageNotAllocated)?; + *dcb = DomainControlBlock::new(domain_id, scheduler_id); + + Ok(domain_id) } - /// Called when event arrives for blocked domain - fn signal_domain(&mut self, id: DomainId) { - let dcb = self.dcb_mut(id); + /// Release a domain ID + pub fn release_domain(&mut self, id: DomainId) -> Result<(), DcbError> { + let word = id.0 as usize / 64; + let bit = id.0 as usize % 64; + + if self.allocated[word] & (1 << bit) == 0 { + return Err(DcbError::NotAllocated); + } - dcb.pending_events.fetch_add(1, Ordering::Release); + // Mark as free + self.allocated[word] &= !(1 << bit); - // If blocked on events, make runnable - if dcb.state.load(Ordering::Acquire) == DomainState::Blocked as u32 { + // Clear DCB + if let Some(dcb) = self.get_mut(id) { dcb.state - .store(DomainState::Runnable as u32, Ordering::Release); + .store(DomainState::Inactive as u32, Ordering::Release); + dcb.id = DomainId::INVALID; } + + Ok(()) } -} -// ## Memory Ordering Considerations + /// Get a DCB by domain ID (immutable) + #[inline] + pub fn get(&self, id: DomainId) -> Option<&DomainControlBlock> { + let page_idx = id.page_index(); + let slot = id.slot_in_page(); -// KERNEL (writer) USERSPACE (reader) -// ─────────────── ────────────────── + self.pages.get(page_idx)?.as_ref()?.get(slot) + } -// // Update multiple fields -// dcb.time_used.store(x, Relaxed); -// dcb.time_remaining.store(y, Relaxed); -// dcb.state.store(z, Release); ──────────────────────┐ -// │ │ -// │ Release ensures all │ -// │ prior writes visible │ -// ▼ ▼ -// let state = dcb.state.load(Acquire); -// // Acquire ensures we see -// // all writes before the Release -// let used = dcb.time_used.load(Relaxed); -// let rem = dcb.time_remaining.load(Relaxed); + /// Get a DCB by domain ID (mutable) - kernel only + #[inline] + pub fn get_mut(&mut self, id: DomainId) -> Option<&mut DomainControlBlock> { + let page_idx = id.page_index(); + let slot = id.slot_in_page(); -// Protocol: -// - Kernel does Release store on state LAST -// - Userspace does Acquire load on state FIRST -// - Then can safely read other fields with Relaxed + self.pages.get_mut(page_idx)?.as_mut()?.get_mut(slot) + } -impl NucleusObject for Domain { - const TYPE: ObjectType = ObjectType::DOMAIN; + /// Get physical address of a DCB page (for user mapping) + pub fn page_phys_addr(&self, page_idx: usize) -> Option { + self.phys_addrs.get(page_idx).copied().flatten() + } + + /// Get user-space virtual address for a domain's DCB + pub fn user_addr(&self, id: DomainId) -> VirtAddr { + VirtAddr::new(Self::USER_BASE.as_u64() + (id.0 as u64 * 128)) + } + + /// Iterate over all allocated domains + pub fn iter_allocated(&self) -> impl Iterator + '_ { + self.allocated + .iter() + .enumerate() + .flat_map(|(word_idx, &word)| { + (0..64).filter_map(move |bit| { + if word & (1 << bit) != 0 { + Some(DomainId((word_idx * 64 + bit) as u32)) + } else { + None + } + }) + }) + } + + /// Number of allocated domains + pub fn num_allocated(&self) -> usize { + self.allocated.iter().map(|w| w.count_ones() as usize).sum() + } + + fn find_free_slot(&self) -> Result { + let max_id = (self.num_pages * DcbPage::DCBS_PER_PAGE as usize) as u32; + + for (word_idx, &word) in self.allocated.iter().enumerate() { + if word != !0 { + let bit = word.trailing_ones(); + let id = (word_idx as u32 * 64) + bit; + if id < max_id { + return Ok(id); + } + } + } + + Err(DcbError::NoFreeDomains) + } } diff --git a/kernel/nucleus/src/objects/mod.rs b/kernel/nucleus/src/objects/mod.rs index 274485a2e..f6feb7a80 100644 --- a/kernel/nucleus/src/objects/mod.rs +++ b/kernel/nucleus/src/objects/mod.rs @@ -1,6 +1,7 @@ pub mod arch; pub mod arch_objects; pub mod debug_console; +pub mod domain; pub mod nucleus; pub mod nucleus_object; pub mod object_pool; diff --git a/kernel/nucleus/src/objects/nucleus.rs b/kernel/nucleus/src/objects/nucleus.rs index 8cb534963..41c2f63bd 100644 --- a/kernel/nucleus/src/objects/nucleus.rs +++ b/kernel/nucleus/src/objects/nucleus.rs @@ -1,4 +1,11 @@ -use crate::objects::{ArchObjects, arch::ArchPools}; +use { + crate::objects::{ArchObjects, arch::ArchPools, domain::DcbPages}, + core::sync::atomic::Ordering, + libobject::{ + KeySlot, + domain::{BlockReason, DomainId, DomainState}, + }, +}; // ┌─────────────────────────────────────────────────────────────────────┐ // │ KERNEL TYPE STRUCTURE │ @@ -59,3 +66,125 @@ pub struct Nucleus { /// DCB shared pages pub dcb_pages: DcbPages, } + +// ═══════════════════════════════════════════════════════════════════ +// KERNEL INTEGRATION +// ═══════════════════════════════════════════════════════════════════ + +impl Nucleus { + /// Update DCB when domain is activated + pub fn activate_domain(&mut self, id: DomainId, time_budget_ns: u64) { + if let Some(dcb) = self.dcb_pages.get_mut(id) { + // Update time budget + dcb.time_remaining_ns + .store(time_budget_ns, Ordering::Relaxed); + dcb.last_activated_ns + .store(self.current_time_ns(), Ordering::Relaxed); + dcb.activation_count.fetch_add(1, Ordering::Relaxed); + dcb.cpu.store(self.current_cpu() as u32, Ordering::Relaxed); + + // Set state last (Release ensures all above writes are visible) + dcb.state + .store(DomainState::Running as u32, Ordering::Release); + } + } + + /// Update DCB when domain yields/blocks/faults + pub fn deactivate_domain(&mut self, id: DomainId, reason: DeactivateReason) { + let elapsed = self.time_since_activation(id); + + if let Some(dcb) = self.dcb_pages.get_mut(id) { + // Update time accounting + dcb.time_consumed_ns.fetch_add(elapsed, Ordering::Relaxed); + dcb.time_remaining_ns.fetch_sub( + elapsed.min(dcb.time_remaining_ns.load(Ordering::Relaxed)), + Ordering::Relaxed, + ); + + // Update state based on reason + match reason { + DeactivateReason::TimeExhausted => { + dcb.state + .store(DomainState::Runnable as u32, Ordering::Release); + } + + DeactivateReason::Blocked { reason, slot } => { + dcb.block_reason.store(reason as u32, Ordering::Relaxed); + dcb.blocked_on_slot.store(slot.0 as u32, Ordering::Relaxed); + dcb.state + .store(DomainState::Blocked as u32, Ordering::Release); + } + + DeactivateReason::Faulted { + fault_type, + code, + addr, + slot, + } => { + dcb.fault_type.store(fault_type as u32, Ordering::Relaxed); + dcb.fault_code.store(code, Ordering::Relaxed); + dcb.fault_addr.store(addr, Ordering::Relaxed); + dcb.fault_slot.store(slot.0 as u32, Ordering::Relaxed); + dcb.state + .store(DomainState::Faulted as u32, Ordering::Release); + } + + DeactivateReason::Suspended => { + dcb.state + .store(DomainState::Suspended as u32, Ordering::Release); + } + + DeactivateReason::Yielded => { + dcb.state + .store(DomainState::Runnable as u32, Ordering::Release); + } + } + } + } + + /// Update DCB when notification is signaled to a domain + pub fn signal_notification(&mut self, id: DomainId, slot: KeySlot, bits: u64) { + if let Some(dcb) = self.dcb_pages.get_mut(id) { + // OR the notification bits + dcb.pending_notifications + .fetch_or(1 << slot.0, Ordering::Release); + + // If domain was blocked on notifications, make it runnable + let state = dcb.state.load(Ordering::Acquire); + let block_reason = dcb.block_reason.load(Ordering::Relaxed); + + if state == DomainState::Blocked as u32 + && block_reason == BlockReason::Notification as u32 + { + dcb.state + .store(DomainState::Runnable as u32, Ordering::Release); + } + } + } +} + +pub enum DeactivateReason { + TimeExhausted, + Blocked { + reason: BlockReason, + slot: KeySlot, + }, + Faulted { + fault_type: FaultType, + code: u32, + addr: u64, + slot: KeySlot, + }, + Suspended, + Yielded, +} + +#[repr(u32)] +pub enum FaultType { + None = 0, + PageFault = 1, + CapFault = 2, + UnknownSyscall = 3, + UserException = 4, + VMFault = 5, +} diff --git a/libs/memory/src/virt_addr.rs b/libs/memory/src/virt_addr.rs index ba5b89052..5b02527a6 100644 --- a/libs/memory/src/virt_addr.rs +++ b/libs/memory/src/virt_addr.rs @@ -14,7 +14,7 @@ use { fmt, ops::{Add, AddAssign, Rem, RemAssign, Sub, SubAssign}, }, - usize_conversions::{FromUsize, usize_from}, + usize_conversions::FromUsize, ux::*, }; @@ -72,12 +72,13 @@ impl VirtAddr { /// This function performs sign extension of bit 47 to make the address canonical, so /// bits 48 to 64 are overwritten. If you want to check that these bits contain no data, /// use `new` or `try_new`. - pub fn new_unchecked(mut addr: u64) -> VirtAddr { - if addr.get_bit(47) { - addr.set_bits(48..64, 0xffff); - } else { - addr.set_bits(48..64, 0); - } + pub const fn new_unchecked(addr: u64) -> VirtAddr { + // FIXME: Constness! + // if addr.get_bit(47) { + // addr.set_bits(48..64, 0xffff); + // } else { + // addr.set_bits(48..64, 0); + // } VirtAddr(addr) } @@ -87,7 +88,7 @@ impl VirtAddr { } /// Converts the address to an `u64`. - pub fn as_u64(self) -> u64 { + pub const fn as_u64(self) -> u64 { self.0 } @@ -98,8 +99,8 @@ impl VirtAddr { /// Converts the address to a raw pointer. #[cfg(target_pointer_width = "64")] - pub fn as_ptr(self) -> *const T { - usize_from(self.as_u64()) as *const T + pub const fn as_ptr(self) -> *const T { + self.0 as *const T } /// Converts the address to a mutable raw pointer. diff --git a/libs/object/Cargo.toml b/libs/object/Cargo.toml index d0dc14080..7335dc927 100644 --- a/libs/object/Cargo.toml +++ b/libs/object/Cargo.toml @@ -19,6 +19,7 @@ publish = false maintenance = { status = "experimental" } [dependencies] +libmemory = { workspace = true } libsyscall = { workspace = true } [lints] diff --git a/libs/object/src/domain.rs b/libs/object/src/domain.rs index 2f4ca8111..3ece9c39c 100644 --- a/libs/object/src/domain.rs +++ b/libs/object/src/domain.rs @@ -1,7 +1,68 @@ +use { + crate::{CapError, Key, KeySlot}, + core::sync::atomic::{AtomicU32, AtomicU64, Ordering}, + libmemory::virt_addr::VirtAddr, + libsyscall::{protected_call0, protected_call2}, +}; + +// ┌─────────────────────────────────────────────────────────────────────┐ +// │ DCB SHARED PAGES ARCHITECTURE │ +// ├─────────────────────────────────────────────────────────────────────┤ +// │ │ +// │ The DCB pages are the Nemesis-inspired mechanism for zero-syscall │ +// │ domain state queries. The kernel maintains DCBs for all domains, │ +// │ mapped read-only into user space. │ +// │ │ +// │ KERNEL VIEW (RW) USER VIEW (RO) │ +// │ ──────────────── ─────────────── │ +// │ │ +// │ 0xFFFF_0000_xxxx_xxxx 0x7FFF_00xx_xxxx_xxxx │ +// │ (kernel linear map) (user mapping) │ +// │ │ │ │ +// │ │ ┌─────────────────────┐ │ │ +// │ └───►│ Physical DCB Page │◄─────────┘ │ +// │ │ ┌───────────────┐ │ │ +// │ │ │ DCB[0] 128B │ │ │ +// │ │ ├───────────────┤ │ │ +// │ │ │ DCB[1] 128B │ │ │ +// │ │ ├───────────────┤ │ │ +// │ │ │ DCB[2] 128B │ │ │ +// │ │ ├───────────────┤ │ │ +// │ │ │ ... │ │ │ +// │ │ ├───────────────┤ │ │ +// │ │ │ DCB[31] 128B │ │ │ +// │ │ └───────────────┘ │ │ +// │ └─────────────────────┘ │ +// │ 4KB page │ +// │ 32 DCBs per page │ +// │ │ +// └─────────────────────────────────────────────────────────────────────┘ + // ================================================== // == Public user interface, usable from userspace == // ================================================== +/// Domain identifier +#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)] +#[repr(transparent)] +pub struct DomainId(pub u32); + +impl DomainId { + pub const INVALID: Self = Self(u32::MAX); + + /// Get the page index for this domain's DCB + #[inline] + pub const fn page_index(&self) -> usize { + (self.0 / DcbPage::DCBS_PER_PAGE) as usize + } + + /// Get the slot within the page + #[inline] + pub const fn slot_in_page(&self) -> usize { + (self.0 % DcbPage::DCBS_PER_PAGE) as usize + } +} + #[repr(u8)] pub enum DomainOp { Activate = 0, // Make domain runnable @@ -13,79 +74,401 @@ pub enum DomainOp { /// Domain capability - handle to a protection domain. /// State queries use shared DCB (no syscall), mutations use CapInvoke. pub struct DomainKey { - cap: Key, + key: Key, id: DomainId, } +enum DomainType {} + impl DomainKey { /// Create a new domain from untyped memory. /// Convenience wrapper around UntypedRetype. - pub fn create(untyped: &mut UntypedCap, dest_slot: KeySlot) -> Result { - // Domains need ~4KB (12 bits) for kernel structures - untyped.retype( - untyped.split(12)?, // Carve off 4KB - ObjectType::Domain, - 12, - dest_slot, - )?; - - // Domain ID is returned in secondary return value - // (or we query it from the newly created DCB) - Ok(DomainKey { - cap: Cap::new(dest_slot), - id: DomainId(/* ... */), - }) - } + // pub fn create(untyped: &mut UntypedCap, dest_slot: KeySlot) -> Result { + // // Domains need ~4KB (12 bits) for kernel structures + // untyped.retype( + // untyped.split(12)?, // Carve off 4KB + // ObjectType::Domain, + // 12, + // dest_slot, + // )?; + // + // // Domain ID is returned in secondary return value + // // (or we query it from the newly created DCB) + // Ok(DomainKey { + // cap: Cap::new(dest_slot), + // id: DomainId(0), + // }) + // } /// Get domain state from shared DCB #[inline] pub fn state(&self) -> DomainState { - let dcb = DCB.get(self.id); + let dcb_view = unsafe { DcbView::from_user_mapping() }; + let dcb = dcb_view.get(self.id).expect("oh well"); DomainState::try_from(dcb.state.load(Ordering::Acquire)).unwrap_or(DomainState::Inactive) } /// Get time used from shared DCB #[inline] pub fn time_used_ns(&self) -> u64 { - let dcb = DCB.get(self.id); - dcb.time_used_ns.load(Ordering::Relaxed) + let dcb_view = unsafe { DcbView::from_user_mapping() }; + let dcb = dcb_view.get(self.id).expect("oh well"); + dcb.time_consumed_ns.load(Ordering::Relaxed) } /// Get pending notifications from shared DCB (NO SYSCALL!) #[inline] pub fn pending_notifications(&self) -> u64 { - let dcb = DCB.get(self.id); + let dcb_view = unsafe { DcbView::from_user_mapping() }; + let dcb = dcb_view.get(self.id).expect("oh well"); dcb.pending_notifications.load(Ordering::Relaxed) } /// Activate domain (make runnable) - requires syscall - pub fn activate(&self) -> Result<(), Error> { - let ret = unsafe { syscall3(self.cap.slot as u64, DomainOp::Activate as u64, 0) }; - Error::from_code(ret) - } - - /// Grant a capability to this domain - requires syscall - pub fn grant(&self, cap: &Cap, dest_slot: CapSlot) -> Result<(), Error> { - let ret = unsafe { - syscall4( - self.cap.slot as u64, - DomainOp::Grant as u64, - cap.slot() as u64, - dest_slot as u64, + pub fn activate(&self) -> Result<(), CapError> { + let (ok, _, _) = unsafe { protected_call0(self.key.slot(), DomainOp::Activate as u32) }; + match ok { + 0 => Ok(()), + _ => Err(CapError::Unknown), + } + } + + /// Grant a capability to this domain + pub fn grant(&self, key: &Key, dest_slot: KeySlot) -> Result<(), CapError> { + let (ok, _, _) = unsafe { + protected_call2( + self.key.slot(), + DomainOp::Grant as u32, + key.slot() as u64, + dest_slot.0 as u64, ) }; - Error::from_code(ret) + match ok { + 0 => Ok(()), + _ => Err(CapError::Unknown), + } } /// Suspend domain - requires syscall - pub fn suspend(&self) -> Result<(), Error> { - let ret = unsafe { syscall3(self.cap.slot as u64, DomainOp::Suspend as u64, 0) }; - Error::from_code(ret) + pub fn suspend(&self) -> Result<(), CapError> { + let (ok, _, _) = unsafe { protected_call0(self.key.slot(), DomainOp::Suspend as u32) }; + match ok { + 0 => Ok(()), + _ => Err(CapError::Unknown), + } } /// Resume suspended domain - requires syscall - pub fn resume(&self) -> Result<(), Error> { - let ret = unsafe { syscall3(self.cap.slot as u64, DomainOp::Resume as u64, 0) }; - Error::from_code(ret) + pub fn resume(&self) -> Result<(), CapError> { + let (ok, _, _) = unsafe { protected_call0(self.key.slot(), DomainOp::Resume as u32) }; + match ok { + 0 => Ok(()), + _ => Err(CapError::Unknown), + } + } +} + +// ═══════════════════════════════════════════════════════════════════ +// DOMAIN CONTROL BLOCK (DCB) +// ═══════════════════════════════════════════════════════════════════ + +/// Domain execution state +#[repr(u32)] +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub enum DomainState { + /// Just created, never activated + Inactive = 0, + /// Ready to run, waiting for CPU time + Runnable = 1, + /// Currently executing on a CPU + Running = 2, + /// Blocked waiting on IPC/notification/event + Blocked = 3, + /// Explicitly suspended by parent + Suspended = 4, + /// Faulted, needs handler + Faulted = 5, + /// Being destroyed + Dying = 6, +} + +/// Why a domain is blocked +#[repr(u32)] +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub enum BlockReason { + None = 0, + Notification = 1, // Waiting on NotificationKey::wait() + EventCount = 2, // Waiting on EventCountKey::await_ge() + EndpointSend = 3, // Blocked on EndpointKey::call() send phase + EndpointRecv = 4, // Blocked on EndpointKey::recv() + Reply = 5, // Waiting for reply after call() + TimeDonation = 6, // Donated time, waiting for return +} + +// pub enum DeactivateReason { +// TimeExhausted, +// BlockedOnEvent(EndpointCap), <-- BlockReason::Endpoint +// Yielded, +// Faulted(fault), +// } + +// ═══════════════════════════════════════════════════════════ +// SHARED SECTION (read-only mapped to userspace) +// ═══════════════════════════════════════════════════════════ + +/// Domain Control Block - shared between kernel and userspace. +/// This is a user-visible half of domain state structure. +/// +/// This is the Nemesis-inspired design for zero-syscall state queries. +/// The kernel writes, userspace reads. +/// +/// Size: 128 bytes (cache-line aligned, 32 per 4KB page) +#[repr(C, align(128))] +pub struct DomainControlBlock { + // ─── Identity ─── + /// Domain ID + pub id: DomainId, + /// Domain name (for debugging) + pub name: [u8; 24], + + // ─── Execution State ─── + /// Current state (use Acquire ordering when reading) + pub state: AtomicU32, // DomainState + /// Why blocked (valid when state == Blocked) + pub block_reason: AtomicU32, // BlockReason (if blocked) + /// Key slot we're blocked on (e.g., which notification) + pub blocked_on_slot: AtomicU32, // Cap slot we're blocked on + /// CPU this domain is running on (or was last running on) + pub cpu: AtomicU32, + + // ─── Time Accounting (QoS) ─── + /// Total CPU time consumed (nanoseconds) + pub time_consumed_ns: AtomicU64, + /// Remaining time in current activation (nanoseconds) + pub time_remaining_ns: AtomicU64, + /// Number of times this domain has been activated + pub activation_count: AtomicU64, + /// Timestamp of last activation (for profiling) + pub last_activated_ns: AtomicU64, + + // ─── Scheduling Parameters ─── + /// Parent scheduler domain + pub scheduler_id: DomainId, + /// Priority (higher = more important) + pub priority: u32, + /// Scheduling flags + pub sched_flags: u32, + /// Period for periodic domains (nanoseconds, 0 = aperiodic) + pub period_ns: u64, + // CPU allocation per period + // pub budget_ns: u64, // slice_ns + // Scheduled deadline (absolute time) + // pub deadline: AtomicU64, + + // ─── Event State ─── + /// Bitmap of pending notification slots + pub pending_notifications: AtomicU64, + /// Count of event counts with pending events + pub pending_event_counts: AtomicU32, + /// Count of pending endpoint messages + pub pending_endpoints: AtomicU32, + // Endpoint that caused last wakeup + // pub last_event_ep: AtomicU32, + + // ─── Fault Information ─── + /// Fault type (valid when state == Faulted) + pub fault_type: AtomicU32, + /// Fault-specific code + pub fault_code: AtomicU32, + /// Fault address (for page faults) + pub fault_addr: AtomicU64, + /// Key slot that caused fault (for cap faults) + pub fault_slot: AtomicU32, +} + +// Compile-time size check +// TODO const _: () = assert!(core::mem::size_of::() == 128); +// TODO const _: () = assert!(core::mem::align_of::() == 128); + +impl DomainControlBlock { + /// Create a new DCB for a domain + pub const fn new(id: DomainId, scheduler_id: DomainId) -> Self { + Self { + id, + scheduler_id, + name: [0; 24], + state: AtomicU32::new(DomainState::Inactive as u32), + block_reason: AtomicU32::new(BlockReason::None as u32), + blocked_on_slot: AtomicU32::new(0), + cpu: AtomicU32::new(0), + time_consumed_ns: AtomicU64::new(0), + time_remaining_ns: AtomicU64::new(0), + activation_count: AtomicU64::new(0), + last_activated_ns: AtomicU64::new(0), + priority: 0, + sched_flags: 0, + period_ns: 0, + pending_notifications: AtomicU64::new(0), + pending_event_counts: AtomicU32::new(0), + pending_endpoints: AtomicU32::new(0), + fault_type: AtomicU32::new(0), + fault_code: AtomicU32::new(0), + fault_addr: AtomicU64::new(0), + fault_slot: AtomicU32::new(0), + } + } + + /// Set domain name (truncated to 8 bytes) + pub fn set_name(&mut self, name: &str) { + let bytes = name.as_bytes(); + let len = bytes.len().min(8); + self.name[..len].copy_from_slice(&bytes[..len]); + self.name[len..].fill(0); + } + + /// Get domain name as string slice + pub fn name_str(&self) -> &str { + let len = self.name.iter().position(|&b| b == 0).unwrap_or(8); + core::str::from_utf8(&self.name[..len]).unwrap_or("???") + } +} + +// ═══════════════════════════════════════════════════════════════════ +// DCB PAGE +// ═══════════════════════════════════════════════════════════════════ + +/// A single 4KB page containing 32 DCBs. +#[repr(C, align(4096))] +pub struct DcbPage { + dcbs: [DomainControlBlock; Self::DCBS_PER_PAGE as usize], +} + +impl DcbPage { + /// Number of DCBs per 4KB page (128 bytes each) + pub const DCBS_PER_PAGE: u32 = 4096 / 128; // = 32 + + /// Create a new page with uninitialized DCBs + pub const fn new() -> Self { + // This is a bit ugly but works at const time + const INIT_DCB: DomainControlBlock = + DomainControlBlock::new(DomainId::INVALID, DomainId::INVALID); + Self { + dcbs: [INIT_DCB; Self::DCBS_PER_PAGE as usize], + } + } + + /// Get a DCB by slot index + #[inline] + pub fn get(&self, slot: usize) -> Option<&DomainControlBlock> { + self.dcbs.get(slot) + } + + /// Get a mutable DCB by slot index + #[inline] + pub fn get_mut(&mut self, slot: usize) -> Option<&mut DomainControlBlock> { + self.dcbs.get_mut(slot) + } +} + +// TODO const _: () = assert!(core::mem::size_of::() == 4096); + +// ═══════════════════════════════════════════════════════════════════ +// DCB VIEW +// Userspace sees: const DCB_BASE: *const DomainControlBlock = DcbPages::USER_BASE; // FIXME: pervasives +// Access DCB n: &*DCB_BASE.add(n) +// ═══════════════════════════════════════════════════════════════════ + +/// User-space view of DCB pages (read-only). +/// +/// This is used by schedulers and other user-space code to query +/// domain state without syscalls. +pub struct DcbView { + base: *const DomainControlBlock, +} + +impl DcbView { + /// Create from the well-known user-space address + /// + /// # Safety + /// Must only be called after kernel has set up the mapping + pub const unsafe fn from_user_mapping() -> Self { + // FIXME: Duplicate DcbPages::USER_BASE const from nucleus/objects/domain.rs here, keep in sync! + const USER_BASE: VirtAddr = VirtAddr::new_unchecked(0x0000_7FFF_FE00_0000); + Self { + base: USER_BASE.as_ptr() as *const DomainControlBlock, + } + } + + /// Get a DCB by domain ID (read-only) + /// + /// Returns None if the domain ID is invalid or not allocated. + /// Note: We can't actually check allocation status from user-space, + /// so we just check if the DCB looks valid. + #[inline] + pub fn get(&self, id: DomainId) -> Option<&DomainControlBlock> { + // FIXME: Duplicate DcbPages::MAX_DOMAINS const from nucleus/objects/domain.rs here, keep in sync! + const MAX_DOMAINS: usize = 8192; + if id.0 >= MAX_DOMAINS as u32 { + return None; + } + + let dcb = unsafe { &*self.base.add(id.0 as usize) }; + + // Basic validity check + if dcb.id != id { + return None; + } + + Some(dcb) + } + + /// Get state of a domain (fast path for schedulers) + #[inline] + pub fn state(&self, id: DomainId) -> Option { + let dcb = self.get(id)?; + let raw = dcb.state.load(Ordering::Acquire); + DomainState::try_from(raw).ok() + } + + /// Check if a domain is runnable + #[inline] + pub fn is_runnable(&self, id: DomainId) -> bool { + self.state(id) == Some(DomainState::Runnable) + } + + /// Get time consumed by a domain + #[inline] + pub fn time_consumed(&self, id: DomainId) -> Option { + let dcb = self.get(id)?; + Some(dcb.time_consumed_ns.load(Ordering::Relaxed)) + } + + /// Get my own DCB + #[inline(always)] + pub fn myself(&self) -> &DomainControlBlock { + // Current domain ID stored in thread-local or well-known register + self.get(DomainId(1)) //current_domain_id() + .expect("Self-domain is always present") + } +} + +// Global user-space accessor (set up during domain init) - FIXME: ? +// In user-space code: +// static DCB: DcbView = unsafe { DcbView::from_user_mapping() }; + +impl TryFrom for DomainState { + type Error = (); + + fn try_from(value: u32) -> Result { + match value { + 0 => Ok(DomainState::Inactive), + 1 => Ok(DomainState::Runnable), + 2 => Ok(DomainState::Running), + 3 => Ok(DomainState::Blocked), + 4 => Ok(DomainState::Suspended), + 5 => Ok(DomainState::Faulted), + 6 => Ok(DomainState::Dying), + _ => Err(()), + } } } diff --git a/libs/object/src/lib.rs b/libs/object/src/lib.rs index 7c8e6c9a3..82e2cb922 100644 --- a/libs/object/src/lib.rs +++ b/libs/object/src/lib.rs @@ -1,6 +1,7 @@ #![no_std] pub mod debug_console; +pub mod domain; pub mod key; pub mod key_table; pub mod object_type; From 72c31a7a9c362ab148c8c31a3a2713c3aa8822b5 Mon Sep 17 00:00:00 2001 From: Berkus Decker Date: Sun, 1 Feb 2026 20:05:53 +0200 Subject: [PATCH 059/107] fix: Compile fixes #7 fix: Compile twists #8 --- kernel/init_thread/Cargo.toml | 1 + kernel/init_thread/src/main.rs | 3 +- kernel/nucleus/src/api/debug_console.rs | 5 +- kernel/nucleus/src/api/key_entry.rs | 13 ++-- kernel/nucleus/src/api/mod.rs | 28 +++++-- kernel/nucleus/src/main.rs | 23 +++--- .../src/objects/arch/aarch64_objects.rs | 76 ++++++++++--------- kernel/nucleus/src/objects/arch/arch_pools.rs | 33 ++++---- kernel/nucleus/src/objects/debug_console.rs | 4 +- kernel/nucleus/src/objects/domain.rs | 15 ++-- kernel/nucleus/src/objects/key_table.rs | 7 +- kernel/nucleus/src/objects/mod.rs | 5 +- kernel/nucleus/src/objects/nucleus.rs | 38 ++++++++-- kernel/nucleus/src/objects/object_ref.rs | 7 ++ libs/object/src/lib.rs | 36 +++++++++ libs/object/src/rights.rs | 1 + 16 files changed, 195 insertions(+), 100 deletions(-) diff --git a/kernel/init_thread/Cargo.toml b/kernel/init_thread/Cargo.toml index 0bb5b9cab..3d568226c 100644 --- a/kernel/init_thread/Cargo.toml +++ b/kernel/init_thread/Cargo.toml @@ -44,6 +44,7 @@ libmemory = { workspace = true } libplatform = { workspace = true } libprint = { workspace = true } libqemu = { workspace = true, optional = true } +libobject = { workspace = true } libsyscall = { workspace = true } libtime = { workspace = true } snafu = { workspace = true } diff --git a/kernel/init_thread/src/main.rs b/kernel/init_thread/src/main.rs index 069ab6a13..2608c802a 100644 --- a/kernel/init_thread/src/main.rs +++ b/kernel/init_thread/src/main.rs @@ -55,6 +55,7 @@ use { libcpu::endless_sleep, liblocking::interface::Mutex, libmemory::{phys_addr::PhysAddr, virt_addr::VirtAddr}, + libobject::{DebugConsoleKey, KeySlot}, libqemu::semi_println, libsyscall::protected_call6, memory::BootAllocator, @@ -468,7 +469,7 @@ pub extern "C" fn init_thread_run(_dtb_ptr: *const u8) -> ! { "DEBCON| Debug output via capability invocation on domain's debug console capability", ); - let err = DebugConsoleKey::new_slot(KeySlot::NULL_CAP); + let err = DebugConsoleKey::new_slot(KeySlot::NULL); err.write("DEBCON| Invalid capability invocation - no output"); // semi_println!("Switching to init domain..."); diff --git a/kernel/nucleus/src/api/debug_console.rs b/kernel/nucleus/src/api/debug_console.rs index 1417a02b0..316018cb0 100644 --- a/kernel/nucleus/src/api/debug_console.rs +++ b/kernel/nucleus/src/api/debug_console.rs @@ -8,14 +8,15 @@ use { // ===================== #[inline] -pub fn invoke(cap: &KeyEntry, op: u32, arg0: u64, arg1: u64) -> SyscallResult { +pub fn invoke(cap: &mut KeyEntry, op: u32, arg0: u64, arg1: u64) -> SyscallResult { let console = cap.as_object_mut::()?; let op = DebugConsoleOp::try_from(op).map_err(|_| CapError::InvalidOperation)?; match op { // DebugConsoleOp::Write => console.handle_write(arg0, arg1), DebugConsoleOp::Write => { - crate::objects::debug_console::DebugConsole::handle_write(arg0, arg1) + crate::objects::debug_console::DebugConsole::handle_write(arg0, arg1)?; + Ok((0, 0)) } _ => Err(CapError::InvalidOperation), } diff --git a/kernel/nucleus/src/api/key_entry.rs b/kernel/nucleus/src/api/key_entry.rs index 2a910a392..a3d7ca320 100644 --- a/kernel/nucleus/src/api/key_entry.rs +++ b/kernel/nucleus/src/api/key_entry.rs @@ -37,7 +37,7 @@ pub struct KeyEntry { rights: Rights, /// Slot of parent capability (for revocation tree) /// 0xFFFF = no parent (root capability) - parent_slot: u16, + parent_slot: u32, /// Badge value (for endpoint discrimination, buffer offset, etc.) badge: u32, /// Generation counter (detect stale capabilities) @@ -49,10 +49,7 @@ impl KeyEntry { /// Create a null/empty entry pub const fn null() -> Self { Self { - object_ref: ObjectRef { - ptr: NonNull::dangling(), - obj_type: ObjectType::NULL, - }, + object_ref: ObjectRef::null(), rights: Rights::empty(), parent_slot: 0xFFFF, badge: 0, @@ -70,7 +67,7 @@ impl KeyEntry { Self { object_ref: ObjectRef::new(object), rights, - parent_slot: parent.map(|s| s.0).unwrap_or(0xFFFF), + parent_slot: parent.map(|s| s.0).unwrap_or(0xFFFF_FFFF), badge, generation: 0, } @@ -79,13 +76,13 @@ impl KeyEntry { /// Check if this entry is valid (not null) #[inline] pub fn is_valid(&self) -> bool { - self.object_ref.obj_type != ObjectType::NULL + self.object_ref.object_type() != ObjectType::NULL } /// Get the object type #[inline] pub fn object_type(&self) -> ObjectType { - self.object_ref.obj_type + self.object_ref.object_type() } /// Get access rights diff --git a/kernel/nucleus/src/api/mod.rs b/kernel/nucleus/src/api/mod.rs index 75776f9ad..4eaf580d3 100644 --- a/kernel/nucleus/src/api/mod.rs +++ b/kernel/nucleus/src/api/mod.rs @@ -30,17 +30,21 @@ pub fn handle_cap_invoke( op: u32, args: &[u64; 6], ) -> Result<(u64, u64), CapError> { - let domain = nucleus.current_domain_mut()?; let slot = KeySlot(cap_slot); - let entry = domain.keytable.lookup_mut(slot)?; - let obj_type = entry.object_type(); + let obj_type = { + let domain = nucleus + .current_domain_mut() + .ok_or(CapError::InvalidDomain)?; + let entry = domain.keytable.lookup_mut(slot)?; + entry.object_type() + }; if obj_type.is_arch() { // Architecture-specific dispatch (less common path) - arch_invoke::(nucleus, entry, obj_type, op, args) + arch_invoke::(nucleus, slot, obj_type, op, args) } else { // Core dispatch (common path) - core_invoke::(nucleus, entry, obj_type, op, args) + core_invoke::(nucleus, slot, obj_type, op, args) } } @@ -48,13 +52,18 @@ pub fn handle_cap_invoke( #[inline(always)] fn core_invoke( nucleus: &mut Nucleus, - entry: &mut KeyEntry, + entry_slot: KeySlot, obj_type: ObjectType, op: u32, args: &[u64; 6], ) -> Result<(u64, u64), CapError> { let core_type = CoreType::try_from(obj_type)?; + let domain = nucleus + .current_domain_mut() + .ok_or(CapError::InvalidDomain)?; + let entry = domain.keytable.lookup_mut(entry_slot)?; + match core_type { CoreType::Null => Err(CapError::NullCapability), @@ -114,13 +123,18 @@ fn core_invoke( #[inline(always)] fn arch_invoke( nucleus: &mut Nucleus, - entry: &mut KeyEntry, + entry_slot: KeySlot, obj_type: ObjectType, op: u32, args: &[u64; 6], ) -> Result<(u64, u64), CapError> { let arch_type = ArchType::try_from(obj_type)?; + let domain = nucleus + .current_domain_mut() + .ok_or(CapError::InvalidDomain)?; + let entry = domain.keytable.lookup_mut(entry_slot)?; + match arch_type { // ArchType::Frame => { // let frame = entry.as_object_mut::()?; diff --git a/kernel/nucleus/src/main.rs b/kernel/nucleus/src/main.rs index 2b01823a0..c805f854f 100644 --- a/kernel/nucleus/src/main.rs +++ b/kernel/nucleus/src/main.rs @@ -23,7 +23,7 @@ #![feature(core_intrinsics)] use { - crate::objects::nucleus::Nucleus, + crate::objects::{Nucleus, ObjectPool, arch::ArchPools, domain::DcbPages}, cfg_if::cfg_if, core::{ arch::asm, @@ -50,8 +50,11 @@ mod vectors; static mut NUCLEUS: IRQSafeNullLock>> = IRQSafeNullLock::new(LazyCell::new(|| Nucleus:: { current_domain: None, - dcb_pages: 0, - pools: objects::nucleus::NucleusPools { arch: 0 }, + dcb_pages: DcbPages::new(), + pools: objects::nucleus::NucleusPools { + domains: unsafe { ObjectPool::new(0x1000 as *mut u8, 4096) }, + arch: unsafe { ArchPools::new() }, + }, })); #[panic_handler] @@ -132,12 +135,12 @@ fn cap_invoke_handler( get_pc() ); - let result = api::handle_cap_invoke( - NUCLEUS.lock(), - cap_slot, - op, - &[arg0, arg1, arg2, arg3, arg4, arg5], - ); + let result = unsafe { + #[allow(static_mut_refs)] + NUCLEUS.lock(|nucleus| { + api::handle_cap_invoke(nucleus, cap_slot, op, &[arg0, arg1, arg2, arg3, arg4, arg5]) + }) + }; // let cap = current_domain().keytable.lookup(cap_slot)?; // let args = &[arg0, arg1, arg2, arg3, arg4, arg5]; // FIXME temp @@ -156,7 +159,7 @@ fn cap_invoke_handler( match result { Ok((v0, v1)) => (0, v0, v1), - Err(e) => (e.code(), 0, 0), + Err(e) => e.code(), } } diff --git a/kernel/nucleus/src/objects/arch/aarch64_objects.rs b/kernel/nucleus/src/objects/arch/aarch64_objects.rs index 26fdd03ad..a34f785aa 100644 --- a/kernel/nucleus/src/objects/arch/aarch64_objects.rs +++ b/kernel/nucleus/src/objects/arch/aarch64_objects.rs @@ -44,7 +44,7 @@ impl ArchObjects for AArch64 { }, ArchType::PageTable => { if size_bits != 12 { - Err(CapError::InvalidSize) + Err(CapError::InvalidSize(size_bits as usize)) } else { Ok(4096) } @@ -62,42 +62,44 @@ impl ArchObjects for AArch64 { size_bits: u8, pools: &mut ArchPools, ) -> Result { - match arch_type { - ArchType::Frame => { - let frame_size = FrameSize::from_bits(size_bits)?; - let frame = AArch64Frame::new(phys_addr, frame_size); - let obj = pools - .frames - .allocate(frame) - .ok_or(CapError::PoolExhausted)?; - Ok(ObjectRef::new(obj)) - } - ArchType::PageTable => { - let pt = AArch64PageTable::new(phys_addr); - let obj = pools - .page_tables - .allocate(pt) - .ok_or(CapError::PoolExhausted)?; - Ok(ObjectRef::new(obj)) - } - ArchType::VSpace => { - let vspace = AArch64VSpace::new(); - let obj = pools - .vspaces - .allocate(vspace) - .ok_or(CapError::PoolExhausted)?; - Ok(ObjectRef::new(obj)) - } - ArchType::ASIDPool => { - let pool = AArch64ASIDPool::new(); - let obj = pools - .asid_pools - .allocate(pool) - .ok_or(CapError::PoolExhausted)?; - Ok(ObjectRef::new(obj)) - } - _ => Err(CapError::UnsupportedArchType(arch_type)), - } + // match arch_type { + // ArchType::Frame => { + // let frame_size = FrameSize::from_bits(size_bits as usize) + // .map_err(|_| CapError::InvalidSize(size_bits as usize))?; + // let frame = AArch64Frame::new(phys_addr, frame_size); + // let obj = pools + // .frames + // .allocate(frame) + // .ok_or(CapError::PoolExhausted)?; + // Ok(ObjectRef::new(obj)) + // } + // ArchType::PageTable => { + // let pt = AArch64PageTable::new(phys_addr); + // let obj = pools + // .page_tables + // .allocate(pt) + // .ok_or(CapError::PoolExhausted)?; + // Ok(ObjectRef::new(obj)) + // } + // ArchType::VSpace => { + // let vspace = AArch64VSpace::new(); + // let obj = pools + // .vspaces + // .allocate(vspace) + // .ok_or(CapError::PoolExhausted)?; + // Ok(ObjectRef::new(obj)) + // } + // ArchType::ASIDPool => { + // let pool = AArch64ASIDPool::new(); + // let obj = pools + // .asid_pools + // .allocate(pool) + // .ok_or(CapError::PoolExhausted)?; + // Ok(ObjectRef::new(obj)) + // } + // _ => Err(CapError::UnsupportedArchType(arch_type)), + // } + Err(CapError::UnsupportedArchType(arch_type)) } // ───────────────────────────────────────────────────────────────── diff --git a/kernel/nucleus/src/objects/arch/arch_pools.rs b/kernel/nucleus/src/objects/arch/arch_pools.rs index 95e785f29..2840f6471 100644 --- a/kernel/nucleus/src/objects/arch/arch_pools.rs +++ b/kernel/nucleus/src/objects/arch/arch_pools.rs @@ -6,11 +6,12 @@ use crate::objects::{ArchObjects, ObjectPool}; /// Pools for architecture-specific objects pub struct ArchPools { - pub frames: ObjectPool, - pub page_tables: ObjectPool, - pub vspaces: ObjectPool, - pub asid_pools: ObjectPool, - pub asids: ObjectPool, + // pub frames: ObjectPool, + // pub page_tables: ObjectPool, + // pub vspaces: ObjectPool, + // pub asid_pools: ObjectPool, + // pub asids: ObjectPool, + _marker: core::marker::PhantomData, // FIXME temp } impl ArchPools { @@ -18,19 +19,19 @@ impl ArchPools { /// /// # Safety /// Memory regions must be valid and non-overlapping - pub unsafe fn new( - frame_mem: (*mut u8, usize), - pt_mem: (*mut u8, usize), - vspace_mem: (*mut u8, usize), - asid_pool_mem: (*mut u8, usize), - asid_mem: (*mut u8, usize), + pub unsafe fn new(// frame_mem: (*mut u8, usize), + // pt_mem: (*mut u8, usize), + // vspace_mem: (*mut u8, usize), + // asid_pool_mem: (*mut u8, usize), + // asid_mem: (*mut u8, usize), ) -> Self { Self { - frames: ObjectPool::new(frame_mem.0, frame_mem.1), - page_tables: ObjectPool::new(pt_mem.0, pt_mem.1), - vspaces: ObjectPool::new(vspace_mem.0, vspace_mem.1), - asid_pools: ObjectPool::new(asid_pool_mem.0, asid_pool_mem.1), - asids: ObjectPool::new(asid_mem.0, asid_mem.1), + // frames: unsafe { ObjectPool::new(frame_mem.0, frame_mem.1) }, + // page_tables: unsafe { ObjectPool::new(pt_mem.0, pt_mem.1) }, + // vspaces: unsafe { ObjectPool::new(vspace_mem.0, vspace_mem.1) }, + // asid_pools: unsafe { ObjectPool::new(asid_pool_mem.0, asid_pool_mem.1) }, + // asids: unsafe { ObjectPool::new(asid_mem.0, asid_mem.1) }, + _marker: core::marker::PhantomData, } } } diff --git a/kernel/nucleus/src/objects/debug_console.rs b/kernel/nucleus/src/objects/debug_console.rs index 2d8c681f9..5feb2fffa 100644 --- a/kernel/nucleus/src/objects/debug_console.rs +++ b/kernel/nucleus/src/objects/debug_console.rs @@ -11,14 +11,14 @@ use { pub struct DebugConsole; impl DebugConsole { - fn handle_write(ptr: u64, len: u64) -> Result<(), CapError> { + pub fn handle_write(ptr: u64, len: u64) -> Result<(), CapError> { let slice = unsafe { slice::from_raw_parts(ptr as *const u8, len as usize) }; let mut buf = [0u8; 4096]; // SAFETY: Need to validate user pointer is valid, need to copy via kernel physmem mapping. buf.copy_from_slice(slice); buf[slice.len()] = 0; let cstr = unsafe { core::ffi::CStr::from_bytes_with_nul(&buf[..=slice.len() + 1]) } - .map_err(CapError::Unknown)?; + .map_err(|_| CapError::Unknown)?; libqemu::semihosting::sys_write0_call(cstr); Ok(()) } diff --git a/kernel/nucleus/src/objects/domain.rs b/kernel/nucleus/src/objects/domain.rs index 7f88d4ad9..85b396653 100644 --- a/kernel/nucleus/src/objects/domain.rs +++ b/kernel/nucleus/src/objects/domain.rs @@ -1,5 +1,5 @@ use { - crate::objects::NucleusObject, + crate::objects::{KeyTable, NucleusObject}, core::{ptr::NonNull, sync::atomic::Ordering}, libmemory::{phys_addr::PhysAddr, virt_addr::VirtAddr}, libobject::{ @@ -14,20 +14,21 @@ use { /// This is a nucleus-visible half of domain structure. /// The DomainControlBlock is user-visible and is defined in libobject. -struct Domain { +pub struct Domain { // ═══════════════════════════════════════════════════════════ // PRIVATE SECTION (kernel only, NOT mapped to userspace) // ═══════════════════════════════════════════════════════════ // // This would be in a separate structure or after a page boundary // - Saved register context - // - Capability space root + // - Capability space (keytable) // - Kernel stack pointer // - Etc. + pub keytable: KeyTable, } // Verify size for cache alignment -const _: () = assert!(core::mem::size_of::() == 4096); +// TODO const _: () = assert!(core::mem::size_of::() == 4096); impl NucleusObject for Domain { const TYPE: ObjectType = ObjectType::DOMAIN; @@ -89,7 +90,7 @@ pub struct DcbPages { /// Array of DCB pages (kernel virtual addresses) pages: [Option<&'static mut DcbPage>; Self::MAX_PAGES], /// Physical addresses of each page (for user mapping) - phys_addrs: [Option>; Self::MAX_PAGES], + phys_addrs: [Option; Self::MAX_PAGES], // TODO: Option> /// Number of allocated pages num_pages: usize, /// Next domain ID to allocate @@ -106,7 +107,7 @@ impl DcbPages { /// Well-known user-space base address for DCB mapping /// This is mapped read-only into all domains - pub const USER_BASE: VirtAddr = VirtAddr::new(0x0000_7FFF_FE00_0000); + pub const USER_BASE: VirtAddr = VirtAddr::new_unchecked(0x0000_7FFF_FE00_0000); /// Create empty DCB pages manager pub const fn new() -> Self { @@ -134,7 +135,7 @@ impl DcbPages { } let idx = self.num_pages; - self.pages[idx] = Some(&mut *page); + self.pages[idx] = unsafe { Some(&mut *page) }; self.phys_addrs[idx] = Some(phys_addr); self.num_pages += 1; diff --git a/kernel/nucleus/src/objects/key_table.rs b/kernel/nucleus/src/objects/key_table.rs index 9fa629aef..ec24534fb 100644 --- a/kernel/nucleus/src/objects/key_table.rs +++ b/kernel/nucleus/src/objects/key_table.rs @@ -1,3 +1,8 @@ +use { + crate::{api::key_entry::KeyEntry, objects::NucleusObject}, + libobject::{CapError, KeySlot, ObjectType, domain::DomainId}, +}; + // ==================== // == Nucleus object == // ==================== @@ -96,5 +101,5 @@ impl KeyTable { // KeyTable is itself a kernel object impl NucleusObject for KeyTable { - const TYPE: ObjectType = ObjectType::KeyTable; + const TYPE: ObjectType = ObjectType::KEY_TABLE; } diff --git a/kernel/nucleus/src/objects/mod.rs b/kernel/nucleus/src/objects/mod.rs index f6feb7a80..909498a54 100644 --- a/kernel/nucleus/src/objects/mod.rs +++ b/kernel/nucleus/src/objects/mod.rs @@ -2,12 +2,13 @@ pub mod arch; pub mod arch_objects; pub mod debug_console; pub mod domain; +pub mod key_table; pub mod nucleus; pub mod nucleus_object; pub mod object_pool; pub mod object_ref; pub use { - arch::ArchObjectsImpl, arch_objects::ArchObjects, debug_console::DebugConsole, - nucleus::Nucleus, nucleus_object::NucleusObject, object_pool::ObjectPool, + arch::ArchObjectsImpl, arch_objects::ArchObjects, debug_console::DebugConsole, domain::Domain, + key_table::KeyTable, nucleus::Nucleus, nucleus_object::NucleusObject, object_pool::ObjectPool, }; diff --git a/kernel/nucleus/src/objects/nucleus.rs b/kernel/nucleus/src/objects/nucleus.rs index 41c2f63bd..5d3a36c14 100644 --- a/kernel/nucleus/src/objects/nucleus.rs +++ b/kernel/nucleus/src/objects/nucleus.rs @@ -1,9 +1,9 @@ use { - crate::objects::{ArchObjects, arch::ArchPools, domain::DcbPages}, + crate::objects::{ArchObjects, Domain, ObjectPool, arch::ArchPools, domain::DcbPages}, core::sync::atomic::Ordering, libobject::{ KeySlot, - domain::{BlockReason, DomainId, DomainState}, + domain::{BlockReason, DomainControlBlock, DomainId, DomainState}, }, }; @@ -44,7 +44,7 @@ use { pub struct NucleusPools { // ─── Core Object Pools ─── // pub untypeds: ObjectPool, - // pub domains: ObjectPool, + pub domains: ObjectPool, // pub keytables: ObjectPool, // pub notifications: ObjectPool, // pub event_counts: ObjectPool, @@ -72,16 +72,40 @@ pub struct Nucleus { // ═══════════════════════════════════════════════════════════════════ impl Nucleus { + pub fn current_cpu(&self) -> usize { + 0 + } + + pub fn current_time_ns(&self) -> u64 { + 0 + } + + /// Nucleus-private domain data, like keytables + pub fn current_domain_mut(&mut self) -> Option<&mut Domain> { + // need objects::Domain here, not DCB! or a tuple + self.pools + .domains + .get_mut(self.current_domain.unwrap_or(0) as usize) + } + + /// User-visible DCB + pub fn current_dcb_mut(&mut self) -> Option<&mut DomainControlBlock> { + // need objects::Domain here, not DCB! or a tuple + self.dcb_pages + .get_mut(DomainId(self.current_domain.unwrap_or(0))) + } + /// Update DCB when domain is activated pub fn activate_domain(&mut self, id: DomainId, time_budget_ns: u64) { + let cpu = self.current_cpu(); + let time = self.current_time_ns(); if let Some(dcb) = self.dcb_pages.get_mut(id) { // Update time budget dcb.time_remaining_ns .store(time_budget_ns, Ordering::Relaxed); - dcb.last_activated_ns - .store(self.current_time_ns(), Ordering::Relaxed); + dcb.last_activated_ns.store(time, Ordering::Relaxed); dcb.activation_count.fetch_add(1, Ordering::Relaxed); - dcb.cpu.store(self.current_cpu() as u32, Ordering::Relaxed); + dcb.cpu.store(cpu as u32, Ordering::Relaxed); // Set state last (Release ensures all above writes are visible) dcb.state @@ -91,7 +115,7 @@ impl Nucleus { /// Update DCB when domain yields/blocks/faults pub fn deactivate_domain(&mut self, id: DomainId, reason: DeactivateReason) { - let elapsed = self.time_since_activation(id); + let elapsed = 0; //self.time_since_activation(id); if let Some(dcb) = self.dcb_pages.get_mut(id) { // Update time accounting diff --git a/kernel/nucleus/src/objects/object_ref.rs b/kernel/nucleus/src/objects/object_ref.rs index 166986dcc..a1254a6ba 100644 --- a/kernel/nucleus/src/objects/object_ref.rs +++ b/kernel/nucleus/src/objects/object_ref.rs @@ -27,6 +27,13 @@ impl ObjectRef { } } + pub const fn null() -> Self { + Self { + ptr: NonNull::dangling(), + obj_type: ObjectType::NULL, + } + } + /// Create from a mutable pointer (for objects in pools) /// /// # Safety diff --git a/libs/object/src/lib.rs b/libs/object/src/lib.rs index 82e2cb922..6bb6eb0a1 100644 --- a/libs/object/src/lib.rs +++ b/libs/object/src/lib.rs @@ -8,6 +8,7 @@ pub mod object_type; pub mod rights; pub use { + debug_console::DebugConsoleKey, key::Key, key_table::KeySlot, object_type::{ArchType, CoreType, ObjectType}, @@ -19,6 +20,7 @@ pub type SyscallResult = core::result::Result<(u64, u64), CapError>; pub enum CapError { Unknown, NullCapability, + InvalidDomain, InvalidPointer, InsufficientRights, NotMapped, @@ -47,3 +49,37 @@ pub enum CapError { InvalidSize(usize), InvalidFrameSize(usize), } + +impl CapError { + pub fn code(self) -> (u64, u64, u64) { + match self { + CapError::Unknown => (1, 0, 0), + CapError::NullCapability => (2, 0, 0), + CapError::InvalidDomain => (3, 0, 0), + CapError::InvalidPointer => (4, 0, 0), + CapError::InsufficientRights => (5, 0, 0), + CapError::NotMapped => (6, 0, 0), + CapError::AlreadyMapped => (7, 0, 0), + CapError::InvalidOperation => (8, 0, 0), + CapError::ASIDPoolExhausted => (9, 0, 0), + CapError::NoASIDAssigned => (10, 0, 0), + CapError::InvalidSlot(s) => (11, s.0 as u64, 0), + CapError::EmptySlot(s) => (12, s.0 as u64, 0), + CapError::SlotOccupied(s) => (13, s.0 as u64, 0), + CapError::NotCoreType(t) => (14, t.as_u8() as u64, 0), + CapError::UnknownCoreType(t) => (15, t as u64, 0), + CapError::UnsupportedCoreType(t) => (16, t as u64, 0), + CapError::NotArchType(t) => (17, t.as_u8() as u64, 0), + CapError::UnknownArchType(t) => (18, t as u64, 0), + CapError::UnsupportedArchType(t) => (19, t as u64, 0), + CapError::InvalidObjectType(t) => (20, t.as_u8() as u64, 0), + CapError::TypeMismatch { expected, found } => { + (21, expected.as_u8() as u64, found.as_u8() as u64) + } + CapError::InsufficientMemory => (22, 0, 0), + CapError::PoolExhausted => (23, 0, 0), + CapError::InvalidSize(s) => (24, s.try_into().unwrap(), 0), + CapError::InvalidFrameSize(s) => (25, s.try_into().unwrap(), 0), + } + } +} diff --git a/libs/object/src/rights.rs b/libs/object/src/rights.rs index e952a4bd3..d679e5257 100644 --- a/libs/object/src/rights.rs +++ b/libs/object/src/rights.rs @@ -1,3 +1,4 @@ +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub struct Rights(pub u8); impl Rights { From 96a0b51cdd4a537a6522d84bd3454deac6d73c8b Mon Sep 17 00:00:00 2001 From: Berkus Decker Date: Mon, 2 Feb 2026 00:49:41 +0200 Subject: [PATCH 060/107] wip: Make a capability call demo work with DebugConsole --- .zed/settings.json | 42 +++++----- kernel/init_thread/build.rs | 77 ++++++++++++------- .../init_thread/kernel_sections.template.rs | 1 + kernel/init_thread/src/loader.rs | 12 +-- kernel/init_thread/src/main.rs | 37 ++++++--- kernel/init_thread/src/memory.rs | 10 +-- kernel/init_thread/src/paging.rs | 74 ++++++++++-------- kernel/nucleus/nucleus.ld | 4 + kernel/nucleus/src/api/debug_console.rs | 5 +- kernel/nucleus/src/api/mod.rs | 16 ++++ kernel/nucleus/src/main.rs | 34 +++++--- kernel/nucleus/src/objects/debug_console.rs | 23 ++++-- kernel/nucleus/src/objects/nucleus.rs | 25 +++++- libs/memory/src/lib.rs | 2 +- libs/object/Cargo.toml | 2 + libs/object/src/debug_console.rs | 8 +- libs/object/src/lib.rs | 1 + 17 files changed, 249 insertions(+), 124 deletions(-) diff --git a/.zed/settings.json b/.zed/settings.json index 1b5b8b199..c1d49f549 100644 --- a/.zed/settings.json +++ b/.zed/settings.json @@ -5,27 +5,27 @@ "rust": { "analyzerTargetDir": true, }, - "checkOnSave": { - "enable": true, - "command": "clippy", - "target": "/Users/berkus/Projects/Metta/vesper/targets/aarch64-metta-none-eabi.json", // $ZED_WORKTREE_ROOT - "extraArgs": [ - "-Zbuild-std=compiler_builtins,core,alloc", - "-Zbuild-std-features=compiler-builtins-mem", - "-Zmacro-backtrace", - "--cfg board_rpi3", - "-Ctarget-cpu=cortex-a53", - "--features qemu", - ], - }, - "cargo": { - "target": "/Users/berkus/Projects/Metta/vesper/targets/aarch64-metta-none-eabi.json", // $ZED_WORKTREE_ROOT - "allTargets": false, - "extraArgs": [ - "-Zbuild-std=compiler_builtins,core,alloc", - "-Zbuild-std-features=compiler-builtins-mem", - ], - }, + // "checkOnSave": { + // "enable": true, + // "command": "clippy", + // "target": "/Users/berkus/Projects/Metta/vesper/targets/aarch64-metta-none-eabi.json", // $ZED_WORKTREE_ROOT + // "extraArgs": [ + // "-Zbuild-std=compiler_builtins,core,alloc", + // "-Zbuild-std-features=compiler-builtins-mem", + // "-Zmacro-backtrace", + // "--cfg board_rpi3", + // "-Ctarget-cpu=cortex-a53", + // "--features qemu", + // ], + // }, + // "cargo": { + // "target": "/Users/berkus/Projects/Metta/vesper/targets/aarch64-metta-none-eabi.json", // $ZED_WORKTREE_ROOT + // "allTargets": false, + // "extraArgs": [ + // "-Zbuild-std=compiler_builtins,core,alloc", + // "-Zbuild-std-features=compiler-builtins-mem", + // ], + // }, "rustfmt": { "extraArgs": ["+nightly"], }, diff --git a/kernel/init_thread/build.rs b/kernel/init_thread/build.rs index cc4f795cc..a01a6562d 100644 --- a/kernel/init_thread/build.rs +++ b/kernel/init_thread/build.rs @@ -3,7 +3,7 @@ use { build_print::info, build_rs::output, - goblin::elf::{Elf, program_header::PT_LOAD, section_header::SHT_NOBITS}, + goblin::elf::{Elf, Sym, program_header::PT_LOAD, section_header::SHT_NOBITS}, std::{ env, fs::{self, File}, @@ -46,8 +46,11 @@ fn main() { // Extract section information let sections = extract_sections(&elf, &elf_bytes, out_path); + // Extract stack mapping information + let stack_virt_bottom = stack_virt_bottom(&elf); + // Generate Rust code - generate_rust_code(§ions, out_path); + generate_rust_code(§ions, stack_virt_bottom, out_path); } /// Extracted section with all metadata needed for loading @@ -217,39 +220,47 @@ fn extract_sections(elf: &Elf, elf_bytes: &[u8], out_path: &Path) -> KernelSecti } } +fn find_symbol(elf: &Elf, symbol_name: &str) -> Option { + for sym in &elf.syms { + if let Some(name) = elf.strtab.get_at(sym.st_name) + && symbol_name == name + { + return Some(sym); + } + } + + None +} + /// Try to find vector table location from symbols fn find_vector_table_from_symbols(elf: &Elf) -> Option { // Common symbol names for exception vectors const VECTOR_SYMBOL: &str = "__vectors"; - for sym in &elf.syms { - if let Some(name) = elf.strtab.get_at(sym.st_name) - && VECTOR_SYMBOL == name - { + if let Some(sym) = find_symbol(elf, VECTOR_SYMBOL) { + info!( + "Found vector table symbol '{}': vaddr=0x{:016X}, size=0x{:X}", + VECTOR_SYMBOL, sym.st_value, sym.st_size + ); + + // Verify 2KB alignment + if sym.st_value & 0x7FF != 0 { info!( - "Found vector table symbol '{}': vaddr=0x{:016X}, size=0x{:X}", - name, sym.st_value, sym.st_size + "Vector table symbol at 0x{:016X} is not 2KB aligned!", + sym.st_value ); - - // Verify 2KB alignment - if sym.st_value & 0x7FF != 0 { - info!( - "Vector table symbol at 0x{:016X} is not 2KB aligned!", - sym.st_value - ); - } - - return Some(VectorTableInfo { - virt_addr: sym.st_value, - // If size is 0, assume standard size of 0x800 - mem_size: if sym.st_size > 0 { - sym.st_size as usize - } else { - 0x800 - }, - alignment: 2048, // VBAR requirement - }); } + + return Some(VectorTableInfo { + virt_addr: sym.st_value, + // If size is 0, assume standard size of 0x800 + mem_size: if sym.st_size > 0 { + sym.st_size as usize + } else { + 0x800 + }, + alignment: 2048, // VBAR requirement + }); } output::error("No vector table symbol found! Kernel must define __vectors symbol"); @@ -257,7 +268,16 @@ fn find_vector_table_from_symbols(elf: &Elf) -> Option { None } -fn generate_rust_code(sections: &KernelSections, out_path: &Path) { +fn stack_virt_bottom(elf: &Elf) -> u64 { + const STACK_VIRT_BOTTOM: &str = "__STACK_VIRT_BOTTOM"; + if let Some(sym) = find_symbol(elf, STACK_VIRT_BOTTOM) { + return sym.st_value; + } + output::error("No stack bottom symbol found! Kernel must define __STACK_VIRT_BOTTOM symbol"); + 0 +} + +fn generate_rust_code(sections: &KernelSections, stack_virt_bottom: u64, out_path: &Path) { use minijinja::{Environment, context}; let mut code = Environment::new(); @@ -319,6 +339,7 @@ fn generate_rust_code(sections: &KernelSections, out_path: &Path) { sections => sections_tmpl, bss => bss_tmpl, vectors => vector_tmpl, + stack_virt_bottom, }; // Write generated code diff --git a/kernel/init_thread/kernel_sections.template.rs b/kernel/init_thread/kernel_sections.template.rs index 1f3ca4b50..9e5f2cce3 100644 --- a/kernel/init_thread/kernel_sections.template.rs +++ b/kernel/init_thread/kernel_sections.template.rs @@ -65,4 +65,5 @@ pub static KERNEL: ImageInfo = ImageInfo { sections: LOADABLE_SECTIONS, bss: BSS_META, vectors: VECTORS_META, + stack_virt_bottom: {{stack_virt_bottom | address}}, }; diff --git a/kernel/init_thread/src/loader.rs b/kernel/init_thread/src/loader.rs index 48d3b4e06..37ee5f65d 100644 --- a/kernel/init_thread/src/loader.rs +++ b/kernel/init_thread/src/loader.rs @@ -51,6 +51,8 @@ pub struct ImageInfo { pub sections: &'static [LoadableSection], /// BSS section metadata (no binary data - must be zeroed) pub bss: SectionMeta, + /// BSS section metadata (no binary data - must be zeroed) + pub stack_virt_bottom: u64, /// Exception vector table metadata (to set up VBAR) pub vectors: SectionMeta, } @@ -112,9 +114,7 @@ pub fn load_kernel(allocator: &mut BootAllocator) -> Result Result Result ! { let reg_prop = DeviceTreeProp::new(reg_prop); + let mut total_memory = 0; + for (mem_addr, mem_size) in reg_prop.payload_pairs_iter() { semi_println!("Memory: {} KiB at offset {}", mem_size / 1024, mem_addr); + total_memory += mem_size; BOOT_INFO.lock(|bi| { bi.insert_region(BootInfoMemRegion { start_inclusive: PhysAddr::new(mem_addr), @@ -288,6 +291,16 @@ pub extern "C" fn init_main(dtb_ptr: *const u8) -> ! { // PHASE 2: Set up page tables // ═══════════════════════════════════════════════════════════════ + let (el1_stack, el1_stack_size) = { + // Allocate EL1 stack + let el1_stack_size = 128; // pages + let el1_stack = allocator + .alloc_pages(el1_stack_size) + .expect("Failed to allocate EL1 stack"); + let el1_stack_size = el1_stack_size * 4096; // 64KiB stack + (el1_stack, el1_stack_size) + }; + let mut mmu_setup = paging::MmuSetup::new(&mut allocator).expect("Failed to create MMU setup"); semi_println!("init_main: Created MmuSetup"); @@ -297,8 +310,14 @@ pub extern "C" fn init_main(dtb_ptr: *const u8) -> ! { semi_println!("init_main: Identity mapped the Init_Thread"); // Create kernel mapping with per-section permissions - paging::create_kernel_mapping(&mut mmu_setup, &kernel_layout) - .expect("Failed to create kernel mapping"); + let (el1_stack_top,) = paging::create_kernel_mapping( + &mut mmu_setup, + &kernel_layout, + total_memory, + el1_stack.0, + el1_stack_size, + ) + .expect("Failed to create kernel mapping"); semi_println!("init_main: Higher-half mapped the nucleus"); // ═══════════════════════════════════════════════════════════════ @@ -313,12 +332,6 @@ pub extern "C" fn init_main(dtb_ptr: *const u8) -> ! { // VBAR is only used after MMU is enabled, so we set the virtual address directly let vbar = kernel_layout.vbar_el1_virt(); - // Allocate EL1 stack - let el1_stack = allocator - .alloc_pages(16) - .expect("Failed to allocate EL1 stack"); - let el1_stack_top = el1_stack.as_u64() + 16 * 4096; - // FIXME: stack must be identity-mapped! semi_println!("init_main: EL1 stack at {el1_stack_top:#016x}, vbar {vbar:#016x}"); // ═══════════════════════════════════════════════════════════════ @@ -343,11 +356,11 @@ pub extern "C" fn init_thread_run(_dtb_ptr: *const u8) -> ! { // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ // Run initial thread further in EL1, seting up the capDL etc. - semi_println!("init_main_run dropped to EL1"); + semi_println!("init_main_run: enabled MMU and dropped to EL1"); unsafe { protected_call6(0, 0, 0, 0, 0, 0, 0, 0); } - semi_println!("init_main_run: Returned from syscall"); + semi_println!("init_main_run: Returned from fake syscall"); // ───────────────────────────────────────────────────────────────────── // Initialize kernel subsystems @@ -466,10 +479,10 @@ pub extern "C" fn init_thread_run(_dtb_ptr: *const u8) -> ! { // We have domain caps here, can use: let dbg = DebugConsoleKey::new(); dbg.write( - "DEBCON| Debug output via capability invocation on domain's debug console capability", + "DEBCON| Debug output via capability invocation on domain's debug console capability\n", ); - let err = DebugConsoleKey::new_slot(KeySlot::NULL); + let err = DebugConsoleKey::new_slot(KeySlot::CAPTBL_SELF); err.write("DEBCON| Invalid capability invocation - no output"); // semi_println!("Switching to init domain..."); diff --git a/kernel/init_thread/src/memory.rs b/kernel/init_thread/src/memory.rs index 6491600e0..45c77fd63 100644 --- a/kernel/init_thread/src/memory.rs +++ b/kernel/init_thread/src/memory.rs @@ -118,15 +118,15 @@ pub struct KernelLayout { pub total_size: usize, /// Section metadata (for page table setup) pub sections: &'static [LoadableSection], - /// BSS physical address + /// BSS physical address (for zeroing) pub bss_phys: PhysAddr, - /// BSS virtual address + /// BSS virtual address (for kernel mapping) pub bss_virt: VirtAddr, /// BSS size pub bss_size: usize, - /// Exception vector table physical address (for VBAR_EL1) - pub vectors_phys: PhysAddr, - /// Exception vector table virtual address + /// Stack virtual address (for kernel mapping) + pub stack_virt_bottom: VirtAddr, + /// Exception vector table virtual address (for VBAR_EL1) pub vectors_virt: VirtAddr, } diff --git a/kernel/init_thread/src/paging.rs b/kernel/init_thread/src/paging.rs index 1858bf48e..2d7759f0a 100644 --- a/kernel/init_thread/src/paging.rs +++ b/kernel/init_thread/src/paging.rs @@ -265,7 +265,10 @@ pub fn create_identity_mapping( pub fn create_kernel_mapping( setup: &mut MmuSetup, layout: &KernelLayout, -) -> Result<(), &'static str> { + max_ram_bytes: u64, + el1_stack: u64, + el1_stack_size: usize, +) -> Result<(u64,), &'static str> { // Map each section with its specific permissions for section in layout.iter_sections() { map_section(setup, §ion)?; @@ -280,41 +283,47 @@ pub fn create_kernel_mapping( // Setup linear physical map (all RAM accessible to nucleus) // TODO: exclude physical memory that covers the kernel image itself! // ───────────────────────────────────────────────────────────────── - /* - // Map all physical memory using 2MB blocks - for i in 0..max_ram_gb { - pts.l1_phys_map[i] = make_table_entry(phys_addr_of(&pts.l2_phys_map[i])); - - for j in 0..512 { - let phys = ((i * 512 + j) as u64) << 21; // 2MB granule -- can try 1Gb granules actually? - pts.l2_phys_map[i][j] = make_block_entry_2mb( - PhysAddr::new(phys), - PageFlags::KERNEL_RW | PageFlags::NORMAL_CACHEABLE, - ); - } - } - // ───────────────────────────────────────────────────────────────── - // Setup device MMIO mappings - // ───────────────────────────────────────────────────────────────── + let perms = MemoryPermissions { + readable: true, + writable: true, + executable: false, + }; - // RPi4 peripherals at 0xFE00_0000 - 0xFF00_0000 - // Map as device memory (non-cacheable, no speculation) - let device_base_phys = 0xFE00_0000_u64; - let l1_idx = 0; // First entry in l1_device + // Map all physical memory using 2MB blocks with a specific offset + // Kernel mapping for phys memory starts at + for i in 0..max_ram_bytes.div_ceil(2 * 1024 * 1024) { + setup.map_block_2mb( + Ttbr::Ttbr1, + VirtAddr::new(libmemory::PHYSICAL_KERNEL_WINDOW + i * 2 * 1024 * 1024), + PhysAddr::new(i * 2 * 1024 * 1024), + perms, + ); + } - pts.l1_device[l1_idx] = make_table_entry(phys_addr_of(&pts.l2_device)); + // Map kernel stack + let stack_bottom = layout.stack_virt_bottom; - // Map 16MB of device space with 2MB blocks - for i in 0..8 { - let phys = device_base_phys + (i as u64 * 0x20_0000); - pts.l2_device[i] = make_block_entry_2mb( - PhysAddr::new(phys), - PageFlags::KERNEL_RW | PageFlags::DEVICE_nGnRnE, - ); - } - */ - Ok(()) + for i in 0..el1_stack_size.div_ceil(4 * 1024) as u64 { + setup.map_page( + Ttbr::Ttbr1, + VirtAddr::new(stack_bottom.0 + i * 4 * 1024), + PhysAddr::new(el1_stack + i * 4 * 1024), + perms, + ); + } + + let stack_virt_top = stack_bottom + el1_stack_size; + + // ───────────────────────────────────────────────────────────────── + // Setup device MMIO mappings + // ───────────────────────────────────────────────────────────────── + // RPi4 peripherals at 0xFE00_0000 - 0xFF00_0000 + // let device_base_phys = 0xFE00_0000_u64; + // Map as device memory (non-cacheable, no speculation) + // create_device_mapping(setup, device_base_phys, VIRT_MMIO, 0x100_0000); // 16MiB + + Ok((stack_virt_top.into(),)) } /// Map a single section with proper permissions @@ -382,6 +391,7 @@ pub fn create_device_mapping( // Use device memory attributes let pte_flags = perms.as_pte_flags() | flags::ATTR_DEVICE; + // PageFlags::KERNEL_RW | PageFlags::DEVICE_nGnRnE, setup.map_page_with_flags( Ttbr::Ttbr1, VirtAddr::new(va), diff --git a/kernel/nucleus/nucleus.ld b/kernel/nucleus/nucleus.ld index 5c361d9ec..4ad2f4548 100644 --- a/kernel/nucleus/nucleus.ld +++ b/kernel/nucleus/nucleus.ld @@ -75,6 +75,10 @@ SECTIONS . = ALIGN(__PAGE_SIZE); /* Align up to page size */ } :segment_data + . += __PAGE_SIZE; /* add guard page between BSS and stack! */ + + __STACK_VIRT_BOTTOM = .; + /* __BSS_END = __BSS_START + ALIGN(SIZEOF(.bss), __PAGE_SIZE); *//* LMA, Physical address */ /* __DATA_END = .; *//* VMA, Virtual address */ diff --git a/kernel/nucleus/src/api/debug_console.rs b/kernel/nucleus/src/api/debug_console.rs index 316018cb0..f8b128ab1 100644 --- a/kernel/nucleus/src/api/debug_console.rs +++ b/kernel/nucleus/src/api/debug_console.rs @@ -1,5 +1,6 @@ use { crate::{api::KeyEntry, objects::DebugConsole}, + libmemory::phys_addr::PhysAddr, libobject::{CapError, Key, SyscallResult, debug_console::DebugConsoleOp}, }; @@ -12,10 +13,12 @@ pub fn invoke(cap: &mut KeyEntry, op: u32, arg0: u64, arg1: u64) -> SyscallResul let console = cap.as_object_mut::()?; let op = DebugConsoleOp::try_from(op).map_err(|_| CapError::InvalidOperation)?; + libqemu::semi_println!("DebugConsole:invoke"); + match op { // DebugConsoleOp::Write => console.handle_write(arg0, arg1), DebugConsoleOp::Write => { - crate::objects::debug_console::DebugConsole::handle_write(arg0, arg1)?; + crate::objects::debug_console::DebugConsole::handle_write(PhysAddr::new(arg0), arg1)?; Ok((0, 0)) } _ => Err(CapError::InvalidOperation), diff --git a/kernel/nucleus/src/api/mod.rs b/kernel/nucleus/src/api/mod.rs index 4eaf580d3..b21767c8b 100644 --- a/kernel/nucleus/src/api/mod.rs +++ b/kernel/nucleus/src/api/mod.rs @@ -31,14 +31,27 @@ pub fn handle_cap_invoke( args: &[u64; 6], ) -> Result<(u64, u64), CapError> { let slot = KeySlot(cap_slot); + libqemu::semi_println!( + "handle_cap_invoke(slot {cap_slot}:op {op}:args[{},{},{},{},{},{}])", + args[0], + args[1], + args[2], + args[3], + args[4], + args[5] + ); let obj_type = { let domain = nucleus .current_domain_mut() .ok_or(CapError::InvalidDomain)?; + libqemu::semi_println!("handle_cap_invoke(got domain)"); let entry = domain.keytable.lookup_mut(slot)?; + libqemu::semi_println!("handle_cap_invoke(got entry)"); entry.object_type() }; + libqemu::semi_println!("handle_cap_invoke(resolved obj_type {})", obj_type.as_u8()); + if obj_type.is_arch() { // Architecture-specific dispatch (less common path) arch_invoke::(nucleus, slot, obj_type, op, args) @@ -64,6 +77,8 @@ fn core_invoke( .ok_or(CapError::InvalidDomain)?; let entry = domain.keytable.lookup_mut(entry_slot)?; + libqemu::semi_println!("core_invoke"); + match core_type { CoreType::Null => Err(CapError::NullCapability), @@ -73,6 +88,7 @@ fn core_invoke( // api::untyped::invoke(untyped, entry.rights(), op, args, &mut nucleus.pools) // } CoreType::DebugConsole => { + libqemu::semi_println!("core_invoke: DebugConsole"); let debug_console = entry.as_object_mut::()?; // DebugConsole::invoke(debug_console, entry.rights(), op, args, nucleus) crate::api::debug_console::invoke(entry, op, args[0], args[1]) diff --git a/kernel/nucleus/src/main.rs b/kernel/nucleus/src/main.rs index c805f854f..0e80a622d 100644 --- a/kernel/nucleus/src/main.rs +++ b/kernel/nucleus/src/main.rs @@ -46,15 +46,21 @@ mod objects; /// Exception vectors triggering syscall handing and general IRQ routing mod vectors; +// TODO: Split this into read-only part, that does not need locks, per-cpu mutable part that does not need locks, +// TODO: Shared atomic counters that do not need locks and shared mutable collections that DO need locks (but should be minority) /// Global kernel state, protected by The Great Kernel Lock static mut NUCLEUS: IRQSafeNullLock>> = - IRQSafeNullLock::new(LazyCell::new(|| Nucleus:: { - current_domain: None, - dcb_pages: DcbPages::new(), - pools: objects::nucleus::NucleusPools { - domains: unsafe { ObjectPool::new(0x1000 as *mut u8, 4096) }, - arch: unsafe { ArchPools::new() }, - }, + IRQSafeNullLock::new(LazyCell::new(|| { + let mut n = Nucleus:: { + current_domain: None, + dcb_pages: DcbPages::new(), + pools: objects::nucleus::NucleusPools { + domains: unsafe { ObjectPool::new(0x1000 as *mut u8, 16384) }, // TODO: proper alloc... + arch: unsafe { ArchPools::new() }, + }, + }; + n.create_domain(); + n })); #[panic_handler] @@ -90,7 +96,7 @@ unsafe extern "C" fn syscall_handler() { "mrs x11, spsr_el1", "stp x10, x11, [sp, #248]", // ELR, SPSR // x0-x7 already in place for Rust function call - "mov x8, sp", // frame pointer for handler + "mov x8, sp", // frame pointer for handler -- FIXME: frame argument from below "bl cap_invoke_handler", // Return values in x0, x1, x2 are already set by handler // Restore context (skip x0, x1, x2 - they hold return values) @@ -128,11 +134,12 @@ fn cap_invoke_handler( arg3: u64, arg4: u64, arg5: u64, - frame: u64, //*mut TrapFrame, + frame: u64, //*mut TrapFrame, x8 contains all saved registers ) -> (u64, u64, u64) { semi_println!( - "CapInvoke SYSCALL(cap: {cap_slot}, op: {op}) happened, we're at 0x{:016X}", - get_pc() + "CapInvoke SYSCALL(cap: {cap_slot}, op: {op}) happened, we're at PC {:#016X}, SP {:#016X}", + get_pc(), + get_sp() ); let result = unsafe { @@ -173,3 +180,8 @@ fn get_pc() -> u64 { } pc } + +fn get_sp() -> u64 { + use aarch64_cpu::registers::Readable; + aarch64_cpu::registers::SP.get() +} diff --git a/kernel/nucleus/src/objects/debug_console.rs b/kernel/nucleus/src/objects/debug_console.rs index 5feb2fffa..e6dbc9c97 100644 --- a/kernel/nucleus/src/objects/debug_console.rs +++ b/kernel/nucleus/src/objects/debug_console.rs @@ -1,6 +1,7 @@ use { crate::objects::NucleusObject, core::slice, + libmemory::phys_addr::PhysAddr, libobject::{CapError, ObjectType}, }; @@ -11,14 +12,26 @@ use { pub struct DebugConsole; impl DebugConsole { - pub fn handle_write(ptr: u64, len: u64) -> Result<(), CapError> { - let slice = unsafe { slice::from_raw_parts(ptr as *const u8, len as usize) }; + pub fn handle_write(ptr: PhysAddr, len: u64) -> Result<(), CapError> { + // libqemu::semi_println!( + // "DebugConsole::handle_write(user ptr {ptr:?}, kernel ptr {:?}, size {})", + // ptr.user_to_kernel(), + // len + // ); + let slice = unsafe { slice::from_raw_parts(ptr.user_to_kernel().as_ptr(), len as usize) }; let mut buf = [0u8; 4096]; + // libqemu::semi_println!( + // "DebugConsole::copy from user to {:#08x}", + // &buf as *const _ as u64 + // ); // SAFETY: Need to validate user pointer is valid, need to copy via kernel physmem mapping. - buf.copy_from_slice(slice); + buf[..len as usize].copy_from_slice(slice); buf[slice.len()] = 0; - let cstr = unsafe { core::ffi::CStr::from_bytes_with_nul(&buf[..=slice.len() + 1]) } - .map_err(|_| CapError::Unknown)?; + let cstr = + unsafe { core::ffi::CStr::from_bytes_with_nul(&buf[..=slice.len()]) }.map_err(|e| { + // libqemu::semi_println!("{e}"); + CapError::Unknown + })?; libqemu::semihosting::sys_write0_call(cstr); Ok(()) } diff --git a/kernel/nucleus/src/objects/nucleus.rs b/kernel/nucleus/src/objects/nucleus.rs index 5d3a36c14..5fecf2de9 100644 --- a/kernel/nucleus/src/objects/nucleus.rs +++ b/kernel/nucleus/src/objects/nucleus.rs @@ -1,5 +1,11 @@ use { - crate::objects::{ArchObjects, Domain, ObjectPool, arch::ArchPools, domain::DcbPages}, + crate::{ + api::key_entry::KeyEntry, + objects::{ + ArchObjects, DebugConsole, Domain, KeyTable, ObjectPool, arch::ArchPools, + domain::DcbPages, + }, + }, core::sync::atomic::Ordering, libobject::{ KeySlot, @@ -95,6 +101,23 @@ impl Nucleus { .get_mut(DomainId(self.current_domain.unwrap_or(0))) } + // TODO: Testing fixture + pub fn create_domain(&mut self) { + self.pools + .domains + .allocate(Domain { + keytable: KeyTable::new(DomainId(0)), + }) + .and_then(|dom| { + let _ = dom.keytable.insert( + libobject::KeySlot(127), + KeyEntry::new(&DebugConsole, libobject::Rights::all(), 0, None), + ); + Some(()) + }) + .expect("Poof") + } + /// Update DCB when domain is activated pub fn activate_domain(&mut self, id: DomainId, time_budget_ns: u64) { let cpu = self.current_cpu(); diff --git a/libs/memory/src/lib.rs b/libs/memory/src/lib.rs index 6355b9848..c33206cab 100644 --- a/libs/memory/src/lib.rs +++ b/libs/memory/src/lib.rs @@ -154,4 +154,4 @@ impl fmt::Display for Address { } } -pub const PHYSICAL_KERNEL_WINDOW: u64 = 0xffff_ffff_0000_0000; +pub const PHYSICAL_KERNEL_WINDOW: u64 = 0xffff_f000_0000_0000; diff --git a/libs/object/Cargo.toml b/libs/object/Cargo.toml index 7335dc927..5eb9ebd04 100644 --- a/libs/object/Cargo.toml +++ b/libs/object/Cargo.toml @@ -20,6 +20,8 @@ maintenance = { status = "experimental" } [dependencies] libmemory = { workspace = true } +libprint = { workspace = true } +libqemu = { workspace = true } libsyscall = { workspace = true } [lints] diff --git a/libs/object/src/debug_console.rs b/libs/object/src/debug_console.rs index df9a62251..c663e2e6d 100644 --- a/libs/object/src/debug_console.rs +++ b/libs/object/src/debug_console.rs @@ -42,7 +42,7 @@ impl DebugConsoleKey { } pub fn write(&self, s: &str) -> Result<(), CapError> { - let (_ok, _, _) = unsafe { + let (ok, r1, r2) = unsafe { libsyscall::protected_call2( self.key.slot(), DebugConsoleOp::Write as u32, @@ -50,6 +50,12 @@ impl DebugConsoleKey { s.len() as u64, ) }; + libqemu::semi_println!( + "Return from DebugConsoleOp::Write with result ({}, {}, {})", + ok, + r1, + r2 + ); Ok(()) } } diff --git a/libs/object/src/lib.rs b/libs/object/src/lib.rs index 6bb6eb0a1..709bbf6cb 100644 --- a/libs/object/src/lib.rs +++ b/libs/object/src/lib.rs @@ -1,4 +1,5 @@ #![no_std] +#![feature(format_args_nl)] pub mod debug_console; pub mod domain; From e80eb93baa9ab7518bcce501d075a4bab6b6e09c Mon Sep 17 00:00:00 2001 From: Berkus Decker Date: Fri, 6 Feb 2026 00:52:25 +0200 Subject: [PATCH 061/107] fix: Cleanup exception context, return values from syscall --- kernel/init_thread/src/el_switch.rs | 15 +- kernel/init_thread/src/main.rs | 12 +- kernel/nucleus/src/main.rs | 139 +++--- libs/exception/src/arch/aarch64/esr_el1.rs | 40 ++ .../src/arch/aarch64/exception/mod.rs | 414 ------------------ .../src/arch/aarch64/exception_context.rs | 83 ++++ libs/exception/src/arch/aarch64/mod.rs | 251 ++++++++++- libs/exception/src/arch/aarch64/spsr_el1.rs | 37 ++ .../src/arch/aarch64/{exception => }/traps.rs | 0 .../arch/aarch64/{exception => }/vectors.S | 0 libs/exception/src/arch/mod.rs | 1 + .../src/{exception => }/asynchronous/mod.rs | 0 libs/exception/src/exception/mod.rs | 29 -- libs/exception/src/lib.rs | 30 +- libs/object/src/debug_console.rs | 2 +- .../bcm/interrupt_controller/mod.rs | 8 +- .../bcm/interrupt_controller/peripheral_ic.rs | 4 +- .../device_driver/bcm/pl011_uart.rs | 4 +- .../raspberrypi/exception/asynchronous.rs | 4 +- 19 files changed, 550 insertions(+), 523 deletions(-) create mode 100644 libs/exception/src/arch/aarch64/esr_el1.rs delete mode 100644 libs/exception/src/arch/aarch64/exception/mod.rs create mode 100644 libs/exception/src/arch/aarch64/exception_context.rs create mode 100644 libs/exception/src/arch/aarch64/spsr_el1.rs rename libs/exception/src/arch/aarch64/{exception => }/traps.rs (100%) rename libs/exception/src/arch/aarch64/{exception => }/vectors.S (100%) rename libs/exception/src/{exception => }/asynchronous/mod.rs (100%) delete mode 100644 libs/exception/src/exception/mod.rs diff --git a/kernel/init_thread/src/el_switch.rs b/kernel/init_thread/src/el_switch.rs index d88934fdb..e23e5ee26 100644 --- a/kernel/init_thread/src/el_switch.rs +++ b/kernel/init_thread/src/el_switch.rs @@ -5,8 +5,8 @@ use { aarch64_cpu::{ asm, registers::{ - ELR_EL2, HCR_EL2, MAIR_EL1, ReadWriteable, SCTLR_EL1, SP_EL1, SPSR_EL2, TCR_EL1, - TTBR0_EL1, TTBR1_EL1, VBAR_EL1, Writeable, + ELR_EL2, HCR_EL2, MAIR_EL1, ReadWriteable, SCTLR_EL1, SP_EL1, SPSR_EL1, SPSR_EL2, + TCR_EL1, TTBR0_EL1, TTBR1_EL1, VBAR_EL1, Writeable, }, }, }; @@ -115,7 +115,16 @@ pub unsafe fn enable_mmu_and_drop_to_el1( + SPSR_EL2::A::Masked + SPSR_EL2::I::Masked + SPSR_EL2::F::Masked - + SPSR_EL2::M::EL1h, // Use SP_EL1 + + SPSR_EL2::M::EL1h, // Use SP_EL1, Return to EL1 + ); + + // TODO: Mark interrupts in EL1 + SPSR_EL1.write( + SPSR_EL1::D::Masked + + SPSR_EL1::A::Masked + + SPSR_EL1::I::Masked + + SPSR_EL1::F::Masked + + SPSR_EL1::M::EL1h, // Use SP_EL1 ); // Set return address and stack diff --git a/kernel/init_thread/src/main.rs b/kernel/init_thread/src/main.rs index 9c0fe0477..14776b500 100644 --- a/kernel/init_thread/src/main.rs +++ b/kernel/init_thread/src/main.rs @@ -45,6 +45,7 @@ mod paging; use { crate::boot_info::{BOOT_INFO, BootInfoMemRegion}, + aarch64_cpu::registers::{SPSR_EL2, Writeable}, core::{panic::PanicInfo, ptr::write_bytes, slice}, device_tree::{DeviceTree, DeviceTreeProp}, fdt_rs::{ @@ -82,6 +83,14 @@ fn dump_memory_map() { #[unsafe(no_mangle)] pub extern "C" fn init_main(dtb_ptr: *const u8) -> ! { + SPSR_EL2.write( + SPSR_EL2::D::Masked + + SPSR_EL2::A::Masked + + SPSR_EL2::I::Masked + + SPSR_EL2::F::Masked + + SPSR_EL2::M::EL1h, // Use SP_EL1/2 + ); + semi_println!("init_main started"); // unsafe { @@ -371,9 +380,6 @@ pub extern "C" fn init_thread_run(_dtb_ptr: *const u8) -> ! { // Initialize per-CPU data structures // percpu::init(); - // Initialize exception vectors - // exceptions::init(); - // Initialize interrupt controller (GIC on RPi4) // let boot_info = unsafe { &BOOT_INFO }; // interrupts::init_gic(boot_info); diff --git a/kernel/nucleus/src/main.rs b/kernel/nucleus/src/main.rs index 0e80a622d..990446daf 100644 --- a/kernel/nucleus/src/main.rs +++ b/kernel/nucleus/src/main.rs @@ -32,11 +32,12 @@ use { time::Duration, }, libcpu::endless_sleep, + libexception::arch::aarch64::ExceptionContext, liblocking::{IRQSafeNullLock, interface::Mutex}, liblog::{info, println, warn}, libmemory::mmu::AccessPermissions, libobject::{ArchType, CapError, KeySlot}, - libqemu::semi_println, + libqemu::{semi_print, semi_println}, }; /// Syscall API - capability invocation handlers @@ -75,50 +76,58 @@ fn panicked(info: &PanicInfo) -> ! { unsafe extern "C" fn syscall_handler() { core::arch::naked_asm!( // Save user context to kernel stack - "sub sp, sp, #272", - "stp x0, x1, [sp, #0]", - "stp x2, x3, [sp, #16]", - "stp x4, x5, [sp, #32]", - "stp x6, x7, [sp, #48]", - "stp x8, x9, [sp, #64]", - "stp x10, x11, [sp, #80]", - "stp x12, x13, [sp, #96]", - "stp x14, x15, [sp, #112]", - "stp x16, x17, [sp, #128]", - "stp x18, x19, [sp, #144]", - "stp x20, x21, [sp, #160]", - "stp x22, x23, [sp, #176]", - "stp x24, x25, [sp, #192]", - "stp x26, x27, [sp, #208]", - "stp x28, x29, [sp, #224]", - "str x30, [sp, #240]", // LR - "mrs x10, elr_el1", - "mrs x11, spsr_el1", - "stp x10, x11, [sp, #248]", // ELR, SPSR + "sub sp, sp, #16 * 17", + "", + "stp x0, x1, [sp, #16 * 0]", + "stp x2, x3, [sp, #16 * 1]", + "stp x4, x5, [sp, #16 * 2]", + "stp x6, x7, [sp, #16 * 3]", + "stp x8, x9, [sp, #16 * 4]", + "stp x10, x11, [sp, #16 * 5]", + "stp x12, x13, [sp, #16 * 6]", + "stp x14, x15, [sp, #16 * 7]", + "stp x16, x17, [sp, #16 * 8]", + "stp x18, x19, [sp, #16 * 9]", + "stp x20, x21, [sp, #16 * 10]", + "stp x22, x23, [sp, #16 * 11]", + "stp x24, x25, [sp, #16 * 12]", + "stp x26, x27, [sp, #16 * 13]", + "stp x28, x29, [sp, #16 * 14]", + "", + "mrs x10, SPSR_EL1", + "mrs x11, ELR_EL1", + "", + "stp x30, x10, [sp, #16 * 15]", + "str x11, [sp, #16 * 16]", // x0-x7 already in place for Rust function call - "mov x8, sp", // frame pointer for handler -- FIXME: frame argument from below + "mov x0, sp", // register frame pointer for handler "bl cap_invoke_handler", - // Return values in x0, x1, x2 are already set by handler - // Restore context (skip x0, x1, x2 - they hold return values) - "ldr x3, [sp, #24]", - "ldp x4, x5, [sp, #32]", - "ldp x6, x7, [sp, #48]", - "ldp x8, x9, [sp, #64]", - "ldp x10, x11, [sp, #248]", - "msr elr_el1, x10", - "msr spsr_el1, x11", - "ldp x10, x11, [sp, #80]", - "ldp x12, x13, [sp, #96]", - "ldp x14, x15, [sp, #112]", - "ldp x16, x17, [sp, #128]", - "ldp x18, x19, [sp, #144]", - "ldp x20, x21, [sp, #160]", - "ldp x22, x23, [sp, #176]", - "ldp x24, x25, [sp, #192]", - "ldp x26, x27, [sp, #208]", - "ldp x28, x29, [sp, #224]", - "ldr x30, [sp, #240]", - "add sp, sp, #272", + "", + // Return values in x0, x1, x2 are set in the trap frame + "ldr x19, [sp, #16 * 16]", + "ldp x30, x20, [sp, #16 * 15]", + "", + "msr ELR_EL1, x19", + "msr SPSR_EL1, x20", + "", + "ldp x0, x1, [sp, #16 * 0]", + "ldp x2, x3, [sp, #16 * 1]", + "ldp x4, x5, [sp, #16 * 2]", + "ldp x6, x7, [sp, #16 * 3]", + "ldp x8, x9, [sp, #16 * 4]", + "ldp x10, x11, [sp, #16 * 5]", + "ldp x12, x13, [sp, #16 * 6]", + "ldp x14, x15, [sp, #16 * 7]", + "ldp x16, x17, [sp, #16 * 8]", + "ldp x18, x19, [sp, #16 * 9]", + "ldp x20, x21, [sp, #16 * 10]", + "ldp x22, x23, [sp, #16 * 11]", + "ldp x24, x25, [sp, #16 * 12]", + "ldp x26, x27, [sp, #16 * 13]", + "ldp x28, x29, [sp, #16 * 14]", + "", + "add sp, sp, #16 * 17", + "", "eret", ); } @@ -126,27 +135,32 @@ unsafe extern "C" fn syscall_handler() { /// Kernel entry point #[unsafe(no_mangle)] fn cap_invoke_handler( - cap_slot: u32, - op: u32, - arg0: u64, - arg1: u64, - arg2: u64, - arg3: u64, - arg4: u64, - arg5: u64, - frame: u64, //*mut TrapFrame, x8 contains all saved registers -) -> (u64, u64, u64) { + // cap_slot: u32, + // op: u32, + // arg0: u64, + // arg1: u64, + // arg2: u64, + // arg3: u64, + // arg4: u64, + // arg5: u64, + frame: &mut ExceptionContext, +) { + let cap_slot = frame.gpr[0] as u32; + let op = frame.gpr[1] as u32; semi_println!( - "CapInvoke SYSCALL(cap: {cap_slot}, op: {op}) happened, we're at PC {:#016X}, SP {:#016X}", + "CapInvoke SYSCALL(cap: {cap_slot}, op: {op}) happened, we're at PC {:#016X}, SP {:#016X}, exception frame @ {:#016X}", get_pc(), - get_sp() + get_sp(), + frame as *mut _ as u64, ); + // semi_println!("{}", frame); + + let args: &[u64; 6] = &frame.gpr[2..=7].try_into().unwrap(); + let result = unsafe { #[allow(static_mut_refs)] - NUCLEUS.lock(|nucleus| { - api::handle_cap_invoke(nucleus, cap_slot, op, &[arg0, arg1, arg2, arg3, arg4, arg5]) - }) + NUCLEUS.lock(|nucleus| api::handle_cap_invoke(nucleus, cap_slot, op, args)) }; // let cap = current_domain().keytable.lookup(cap_slot)?; @@ -164,9 +178,16 @@ fn cap_invoke_handler( // ObjectType::None => Err(SyscallError::InvalidSlot), // }; - match result { + let (x0, x1, x2) = match result { Ok((v0, v1)) => (0, v0, v1), Err(e) => e.code(), + }; + // Return values + semi_println!("CapInvoke SYSCALL(Return {x0:#x}, {x1:#x}, {x2:#x})",); + unsafe { + frame.gpr[0] = x0; + frame.gpr[1] = x1; + frame.gpr[2] = x2; } } diff --git a/libs/exception/src/arch/aarch64/esr_el1.rs b/libs/exception/src/arch/aarch64/esr_el1.rs new file mode 100644 index 000000000..d1721a034 --- /dev/null +++ b/libs/exception/src/arch/aarch64/esr_el1.rs @@ -0,0 +1,40 @@ +use {aarch64_cpu::registers::ESR_EL1, core::fmt, tock_registers::LocalRegisterCopy}; + +// Exception syndrome register. +#[repr(transparent)] +pub struct EsrEL1(pub LocalRegisterCopy); + +impl EsrEL1 { + #[inline(always)] + pub fn exception_class(&self) -> Option { + self.0.read_as_enum(ESR_EL1::EC) + } + + #[cfg(any(test, feature = "test_build"))] + #[inline(always)] + pub fn iss(&self) -> u64 { + self.0.read(ESR_EL1::ISS) + } +} + +/// Human readable `ESR_EL1`. +#[rustfmt::skip] +impl fmt::Display for EsrEL1 { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + // Raw print of whole register. + writeln!(f, "ESR_EL1: {:#010x}", self.0.get())?; + + // Raw print of exception class. + write!(f, " Exception Class (EC) : {:#x}", self.0.read(ESR_EL1::EC))?; + + // Exception class. + let ec_translation = match self.exception_class() { + Some(ESR_EL1::EC::Value::DataAbortCurrentEL) => "Data Abort, current EL", + _ => "N/A", + }; + writeln!(f, " - {ec_translation}")?; + + // Raw print of instruction specific syndrome. + write!(f, " Instr Specific Syndrome (ISS): {:#x}", self.0.read(ESR_EL1::ISS)) + } +} diff --git a/libs/exception/src/arch/aarch64/exception/mod.rs b/libs/exception/src/arch/aarch64/exception/mod.rs deleted file mode 100644 index 2b1b17b87..000000000 --- a/libs/exception/src/arch/aarch64/exception/mod.rs +++ /dev/null @@ -1,414 +0,0 @@ -/* - * SPDX-License-Identifier: BlueOak-1.0.0 - * Copyright (c) Berkus Decker - */ - -//! Interrupt handling -//! -//! The base address is given by `VBAR_ELn` and each entry has a defined offset from this -//! base address. Each table has 16 entries, with each entry being 128 bytes (32 instructions) -//! in size. The table effectively consists of 4 sets of 4 entries. -//! -//! Minimal implementation to help catch MMU traps. -//! Reads `ESR_ELx` to understand why trap was taken. -//! -//! `VBAR_EL1`, `VBAR_EL2`, `VBAR_EL3` -//! -//! `CurrentEL` with `SP0`: +0x0 -//! -//! * Synchronous -//! * IRQ/vIRQ -//! * FIQ -//! * SError/vSError -//! -//! `CurrentEL` with `SPx`: +0x200 -//! -//! * Synchronous -//! * IRQ/vIRQ -//! * FIQ -//! * SError/vSError -//! -//! Lower EL using `AArch64`: +0x400 -//! -//! * Synchronous -//! * IRQ/vIRQ -//! * FIQ -//! * SError/vSError -//! -//! Lower EL using `AArch32`: +0x600 -//! -//! * Synchronous -//! * IRQ/vIRQ -//! * FIQ -//! * SError/vSError -//! -//! When the processor takes an exception to `AArch64` execution state, -//! all of the PSTATE interrupt masks is set automatically. This means -//! that further exceptions are disabled. If software is to support -//! nested exceptions, for example, to allow a higher priority interrupt -//! to interrupt the handling of a lower priority source, then software needs -//! to explicitly re-enable interrupts - -use { - crate::exception::PrivilegeLevel, - aarch64_cpu::{asm::barrier, registers::*}, - core::{ - cell::UnsafeCell, - fmt::{self}, - }, - liblog::info, - snafu::Snafu, - tock_registers::{ - LocalRegisterCopy, - interfaces::{Readable, Writeable}, - }, -}; - -mod traps; - -core::arch::global_asm!(include_str!("vectors.S")); - -//-------------------------------------------------------------------------------------------------- -// Private Definitions -//-------------------------------------------------------------------------------------------------- - -/// The exception context as it is stored on the stack on exception entry. -/// Keep in sync with exception setup code in vectors.S! -#[repr(C)] -struct ExceptionContext { - /// General Purpose Registers, x0-x29 - gpr: [u64; 30], - - /// The link register, aka x30. - lr: u64, - - /// Saved program status. - spsr_el1: SpsrEL1, - - /// Exception link register. The program counter at the time the exception happened. - elr_el1: u64, -} - -//-------------------------------------------------------------------------------------------------- -// Private Code -//-------------------------------------------------------------------------------------------------- - -/// The default exception, invoked for every exception type unless the handler -/// is overridden. -/// Prints verbose information about the exception and then panics. -/// -/// Default pointer is configured in the linker script. -#[unsafe(no_mangle)] -extern "C" fn default_exception_handler(exc: &ExceptionContext) { - panic!( - "Unexpected CPU Exception!\n\n\ - {}", - exc - ); -} - -//------------------------------------------------------------------------------ -// Current, EL0 -//------------------------------------------------------------------------------ - -#[unsafe(no_mangle)] -extern "C" fn current_el0_synchronous(_e: &mut ExceptionContext) { - panic!("Should not be here. Use of SP_EL0 in EL1 is not supported.") -} - -#[unsafe(no_mangle)] -extern "C" fn current_el0_irq(_e: &mut ExceptionContext) { - panic!("Should not be here. Use of SP_EL0 in EL1 is not supported.") -} - -#[unsafe(no_mangle)] -extern "C" fn current_el0_serror(_e: &mut ExceptionContext) { - panic!("Should not be here. Use of SP_EL0 in EL1 is not supported.") -} - -//------------------------------------------------------------------------------ -// Current, ELx -//------------------------------------------------------------------------------ - -#[unsafe(no_mangle)] -extern "C" fn current_elx_synchronous(e: &mut ExceptionContext) { - #[cfg(any(test, feature = "test_build"))] - { - const TEST_SVC_ID: u64 = 0x1337; - - let esr_el1 = EsrEL1(LocalRegisterCopy::new(ESR_EL1.get())); - - if let Some(ESR_EL1::EC::Value::SVC64) = esr_el1.exception_class() - && esr_el1.iss() == TEST_SVC_ID - { - liblog::println!("Serving syscall {TEST_SVC_ID}"); - return; - } - } - - if traps::synchronous_common(e) { - return; - } - - default_exception_handler(e); -} - -#[unsafe(no_mangle)] -extern "C" fn current_elx_irq(_e: &mut ExceptionContext) { - // -- @todo - // let token = unsafe { &exception::asynchronous::IRQContext::new() }; - // exception::asynchronous::irq_manager().handle_pending_irqs(token); -} - -#[unsafe(no_mangle)] -extern "C" fn current_elx_serror(e: &mut ExceptionContext) { - default_exception_handler(e); -} - -//------------------------------------------------------------------------------ -// Lower, AArch64 -//------------------------------------------------------------------------------ - -#[unsafe(no_mangle)] -extern "C" fn lower_aarch64_synchronous(e: &mut ExceptionContext) { - default_exception_handler(e); -} - -#[unsafe(no_mangle)] -extern "C" fn lower_aarch64_irq(e: &mut ExceptionContext) { - default_exception_handler(e); -} - -#[unsafe(no_mangle)] -extern "C" fn lower_aarch64_serror(e: &mut ExceptionContext) { - default_exception_handler(e); -} - -//------------------------------------------------------------------------------ -// Lower, AArch32 -//------------------------------------------------------------------------------ - -#[unsafe(no_mangle)] -extern "C" fn lower_aarch32_synchronous(e: &mut ExceptionContext) { - default_exception_handler(e); -} - -#[unsafe(no_mangle)] -extern "C" fn lower_aarch32_irq(e: &mut ExceptionContext) { - default_exception_handler(e); -} - -#[unsafe(no_mangle)] -extern "C" fn lower_aarch32_serror(e: &mut ExceptionContext) { - default_exception_handler(e); -} - -//------------------------------------------------------------------------------ -// Misc -// Wrappers for memory copies of registers. -//------------------------------------------------------------------------------ - -struct SpsrEL1(pub LocalRegisterCopy); -// Exception syndrome register. -struct EsrEL1(pub LocalRegisterCopy); - -/// Human readable `SPSR_EL1`. -#[rustfmt::skip] -impl fmt::Display for SpsrEL1 { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - // Raw value. - writeln!(f, "SPSR_EL1: {:#010x}", self.0.get())?; - - let to_flag_str = |x| -> _ { - if x { "Set" } else { "Not set" } - }; - - writeln!(f, " Flags:")?; - writeln!(f, " Negative (N): {}", to_flag_str(self.0.is_set(SPSR_EL1::N)))?; - writeln!(f, " Zero (Z): {}", to_flag_str(self.0.is_set(SPSR_EL1::Z)))?; - writeln!(f, " Carry (C): {}", to_flag_str(self.0.is_set(SPSR_EL1::C)))?; - writeln!(f, " Overflow (V): {}", to_flag_str(self.0.is_set(SPSR_EL1::V)))?; - - let to_mask_str = |x| -> _ { - if x { "Masked" } else { "Unmasked" } - }; - - writeln!(f, " Exception handling state:")?; - writeln!(f, " Debug (D): {}", to_mask_str(self.0.is_set(SPSR_EL1::D)))?; - writeln!(f, " SError (A): {}", to_mask_str(self.0.is_set(SPSR_EL1::A)))?; - writeln!(f, " IRQ (I): {}", to_mask_str(self.0.is_set(SPSR_EL1::I)))?; - writeln!(f, " FIQ (F): {}", to_mask_str(self.0.is_set(SPSR_EL1::F)))?; - - write!(f, " Illegal Execution State (IL): {}", - to_flag_str(self.0.is_set(SPSR_EL1::IL)) - ) - } -} - -impl EsrEL1 { - #[inline(always)] - fn exception_class(&self) -> Option { - self.0.read_as_enum(ESR_EL1::EC) - } - - #[cfg(any(test, feature = "test_build"))] - #[inline(always)] - fn iss(&self) -> u64 { - self.0.read(ESR_EL1::ISS) - } -} - -/// Human readable `ESR_EL1`. -#[rustfmt::skip] -impl fmt::Display for EsrEL1 { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - // Raw print of whole register. - writeln!(f, "ESR_EL1: {:#010x}", self.0.get())?; - - // Raw print of exception class. - write!(f, " Exception Class (EC) : {:#x}", self.0.read(ESR_EL1::EC))?; - - // Exception class. - let ec_translation = match self.exception_class() { - Some(ESR_EL1::EC::Value::DataAbortCurrentEL) => "Data Abort, current EL", - _ => "N/A", - }; - writeln!(f, " - {ec_translation}")?; - - // Raw print of instruction specific syndrome. - write!(f, " Instr Specific Syndrome (ISS): {:#x}", self.0.read(ESR_EL1::ISS)) - } -} - -//------------------------------------------------------------------------------ -// Misc -// Exception context. -//------------------------------------------------------------------------------ - -impl ExceptionContext { - // #[inline(always)] - // fn exception_class(&self) -> Option { - // self.esr_el1.exception_class() - // } - - #[inline(always)] - fn fault_address_valid() -> bool { - use ESR_EL1::EC::Value::{ - DataAbortCurrentEL, DataAbortLowerEL, InstrAbortCurrentEL, InstrAbortLowerEL, - PCAlignmentFault, WatchpointCurrentEL, WatchpointLowerEL, - }; - - let esr_el1 = EsrEL1(LocalRegisterCopy::new(ESR_EL1.get())); - - match esr_el1.exception_class() { - None => false, - Some(ec) => matches!( - ec, - InstrAbortLowerEL - | InstrAbortCurrentEL - | PCAlignmentFault - | DataAbortLowerEL - | DataAbortCurrentEL - | WatchpointLowerEL - | WatchpointCurrentEL - ), - } - } - - pub fn write_gprs(&self, f: &mut fmt::Formatter) -> fmt::Result { - writeln!(f, "General purpose registers:")?; - - let alternating = |x| -> _ { if x % 2 == 0 { " " } else { "\n" } }; - - // Print two registers per line. - for (i, reg) in self.gpr.iter().enumerate() { - write!(f, " x{: <2}: {: >#018x}{}", i, reg, alternating(i))?; - } - Ok(()) - } -} - -/// Human readable print of the exception context. -impl fmt::Display for ExceptionContext { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - // writeln!(f, "{}", self.esr_el1)?; - - if Self::fault_address_valid() { - writeln!( - f, - "FAR_EL1: {:#018x}", - usize::try_from(FAR_EL1.get()).unwrap_or(0) - )?; - } - - writeln!(f, "{}", self.spsr_el1)?; - writeln!(f, "ELR_EL1: {:#018x} (return to)", self.elr_el1)?; - writeln!(f)?; - self.write_gprs(f)?; - write!(f, " lr : {:#018x}", self.lr) - } -} - -//-------------------------------------------------------------------------------------------------- -// Public Code -//-------------------------------------------------------------------------------------------------- - -/// The processor's current privilege level. -pub fn current_privilege_level() -> (PrivilegeLevel, &'static str) { - let el = CurrentEL.read_as_enum(CurrentEL::EL); - match el { - Some(CurrentEL::EL::Value::EL3) => (PrivilegeLevel::Unknown, "EL3"), - Some(CurrentEL::EL::Value::EL2) => (PrivilegeLevel::Hypervisor, "EL2"), - Some(CurrentEL::EL::Value::EL1) => (PrivilegeLevel::Kernel, "EL1"), - Some(CurrentEL::EL::Value::EL0) => (PrivilegeLevel::User, "EL0"), - _ => (PrivilegeLevel::Unknown, "Unknown"), - } -} - -/// Init exception handling by setting the exception vector base address register. -/// -/// - Changes the HW state of the executing core. -/// - The vector table and the symbol `__EXCEPTION_VECTORS_START` from the linker script must -/// adhere to the alignment and size constraints demanded by the ARMv8-A Architecture Reference -/// Manual. -pub fn handling_init() { - unsafe extern "Rust" { - static __EXCEPTION_VECTORS_START: UnsafeCell<()>; - } - - // Safety: __EXCEPTION_VECTORS_START is a valid pointer to exception vector table - // that is properly aligned and accessible during system initialization - unsafe { - set_vbar_el1_checked(__EXCEPTION_VECTORS_START.get() as u64) - .expect("Vector table properly aligned!"); - } - info!("[!] Exception traps set up"); -} - -/// Errors possibly returned from the traps module. -/// @todo a big over-engineered here. -#[derive(Debug, Snafu)] -enum Error { - /// IVT address is unaligned. - #[snafu(display("Unaligned base address for interrupt vector table"))] - Unaligned, -} - -/// Configure base address of interrupt vectors table. -/// Checks that address is properly 2KiB aligned. -/// -/// # Safety -/// -/// Totally unsafe in the land of the hardware. -unsafe fn set_vbar_el1_checked(vec_base_addr: u64) -> Result<(), Error> { - if vec_base_addr.trailing_zeros() < 11 { - return Err(Error::Unaligned); - } - - VBAR_EL1.set(vec_base_addr); - - // Force VBAR update to complete before next instruction. - barrier::isb(barrier::SY); - - Ok(()) -} diff --git a/libs/exception/src/arch/aarch64/exception_context.rs b/libs/exception/src/arch/aarch64/exception_context.rs new file mode 100644 index 000000000..9a09c0cd3 --- /dev/null +++ b/libs/exception/src/arch/aarch64/exception_context.rs @@ -0,0 +1,83 @@ +use { + aarch64_cpu::registers::{ESR_EL1, FAR_EL1, Readable}, + core::fmt, + tock_registers::LocalRegisterCopy, +}; + +/// The exception context as it is stored on the stack on exception entry. +/// Keep in sync with exception setup code in vectors.S! +#[repr(C)] +pub struct ExceptionContext { + /// General Purpose Registers, x0-x29 + pub gpr: [u64; 30], + /// The link register, aka x30. + pub lr: u64, + /// Saved program status. + pub spsr_el1: super::spsr_el1::SpsrEL1, + /// Exception link register. The program counter at the time the exception happened. + pub elr_el1: u64, +} + +impl ExceptionContext { + // #[inline(always)] + // fn exception_class(&self) -> Option { + // self.esr_el1.exception_class() + // } + + #[inline(always)] + fn fault_address_valid() -> bool { + use ESR_EL1::EC::Value::{ + DataAbortCurrentEL, DataAbortLowerEL, InstrAbortCurrentEL, InstrAbortLowerEL, + PCAlignmentFault, WatchpointCurrentEL, WatchpointLowerEL, + }; + + let esr_el1 = super::esr_el1::EsrEL1(LocalRegisterCopy::new(ESR_EL1.get())); + + match esr_el1.exception_class() { + None => false, + Some(ec) => matches!( + ec, + InstrAbortLowerEL + | InstrAbortCurrentEL + | PCAlignmentFault + | DataAbortLowerEL + | DataAbortCurrentEL + | WatchpointLowerEL + | WatchpointCurrentEL + ), + } + } + + pub fn write_gprs(&self, f: &mut fmt::Formatter) -> fmt::Result { + writeln!(f, "General purpose registers:")?; + + let alternating = |x| -> _ { if x % 2 == 0 { " " } else { "\n" } }; + + // Print two registers per line. + for (i, reg) in self.gpr.iter().enumerate() { + write!(f, " x{: <2}: {: >#018x}{}", i, reg, alternating(i))?; + } + Ok(()) + } +} + +/// Human readable print of the exception context. +impl fmt::Display for ExceptionContext { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + // writeln!(f, "{}", self.esr_el1)?; + + if Self::fault_address_valid() { + writeln!( + f, + "FAR_EL1: {:#018x}", + usize::try_from(FAR_EL1.get()).unwrap_or(0) + )?; + } + + writeln!(f, "{}", self.spsr_el1)?; + writeln!(f, "ELR_EL1: {:#018x} (return to)", self.elr_el1)?; + writeln!(f)?; + self.write_gprs(f)?; + write!(f, " lr : {:#018x}", self.lr) + } +} diff --git a/libs/exception/src/arch/aarch64/mod.rs b/libs/exception/src/arch/aarch64/mod.rs index aa4b15a44..4d6ef2fa6 100644 --- a/libs/exception/src/arch/aarch64/mod.rs +++ b/libs/exception/src/arch/aarch64/mod.rs @@ -1 +1,250 @@ -pub mod exception; +/* + * SPDX-License-Identifier: BlueOak-1.0.0 + * Copyright (c) Berkus Decker + */ + +//! Interrupt handling +//! +//! The base address is given by `VBAR_ELn` and each entry has a defined offset from this +//! base address. Each table has 16 entries, with each entry being 128 bytes (32 instructions) +//! in size. The table effectively consists of 4 sets of 4 entries. +//! +//! Minimal implementation to help catch MMU traps. +//! Reads `ESR_ELx` to understand why trap was taken. +//! +//! `VBAR_EL1`, `VBAR_EL2`, `VBAR_EL3` +//! +//! `CurrentEL` with `SP0`: +0x0 +//! +//! * Synchronous +//! * IRQ/vIRQ +//! * FIQ +//! * SError/vSError +//! +//! `CurrentEL` with `SPx`: +0x200 +//! +//! * Synchronous +//! * IRQ/vIRQ +//! * FIQ +//! * SError/vSError +//! +//! Lower EL using `AArch64`: +0x400 +//! +//! * Synchronous +//! * IRQ/vIRQ +//! * FIQ +//! * SError/vSError +//! +//! Lower EL using `AArch32`: +0x600 +//! +//! * Synchronous +//! * IRQ/vIRQ +//! * FIQ +//! * SError/vSError +//! +//! When the processor takes an exception to `AArch64` execution state, +//! all of the PSTATE interrupt masks is set automatically. This means +//! that further exceptions are disabled. If software is to support +//! nested exceptions, for example, to allow a higher priority interrupt +//! to interrupt the handling of a lower priority source, then software needs +//! to explicitly re-enable interrupts + +use { + crate::PrivilegeLevel, + aarch64_cpu::{asm::barrier, registers::*}, + core::cell::UnsafeCell, + liblog::info, + snafu::Snafu, + tock_registers::interfaces::{Readable, Writeable}, +}; + +mod esr_el1; +mod exception_context; +mod spsr_el1; +mod traps; + +pub use exception_context::ExceptionContext; + +core::arch::global_asm!(include_str!("vectors.S")); + +//-------------------------------------------------------------------------------------------------- +// Private Definitions +//-------------------------------------------------------------------------------------------------- + +//-------------------------------------------------------------------------------------------------- +// Private Code +//-------------------------------------------------------------------------------------------------- + +/// The default exception, invoked for every exception type unless the handler +/// is overridden. +/// Prints verbose information about the exception and then panics. +/// +/// Default pointer is configured in the linker script. +#[unsafe(no_mangle)] +extern "C" fn default_exception_handler(exc: &ExceptionContext) { + panic!( + "Unexpected CPU Exception!\n\n\ + {}", + exc + ); +} + +//------------------------------------------------------------------------------ +// Current, EL0 +//------------------------------------------------------------------------------ + +#[unsafe(no_mangle)] +extern "C" fn current_el0_synchronous(_e: &mut ExceptionContext) { + panic!("Should not be here. Use of SP_EL0 in EL1 is not supported.") +} + +#[unsafe(no_mangle)] +extern "C" fn current_el0_irq(_e: &mut ExceptionContext) { + panic!("Should not be here. Use of SP_EL0 in EL1 is not supported.") +} + +#[unsafe(no_mangle)] +extern "C" fn current_el0_serror(_e: &mut ExceptionContext) { + panic!("Should not be here. Use of SP_EL0 in EL1 is not supported.") +} + +//------------------------------------------------------------------------------ +// Current, ELx +//------------------------------------------------------------------------------ + +#[unsafe(no_mangle)] +extern "C" fn current_elx_synchronous(e: &mut ExceptionContext) { + #[cfg(any(test, feature = "test_build"))] + { + const TEST_SVC_ID: u64 = 0x1337; + + let esr_el1 = esr_el1::EsrEL1(LocalRegisterCopy::new(ESR_EL1.get())); + + if let Some(ESR_EL1::EC::Value::SVC64) = esr_el1.exception_class() + && esr_el1.iss() == TEST_SVC_ID + { + liblog::println!("Serving syscall {TEST_SVC_ID}"); + return; + } + } + + if traps::synchronous_common(e) { + return; + } + + default_exception_handler(e); +} + +#[unsafe(no_mangle)] +extern "C" fn current_elx_irq(_e: &mut ExceptionContext) { + // -- @todo + // let token = unsafe { &exception::asynchronous::IRQContext::new() }; + // exception::asynchronous::irq_manager().handle_pending_irqs(token); +} + +#[unsafe(no_mangle)] +extern "C" fn current_elx_serror(e: &mut ExceptionContext) { + default_exception_handler(e); +} + +//------------------------------------------------------------------------------ +// Lower, AArch64 +//------------------------------------------------------------------------------ + +#[unsafe(no_mangle)] +extern "C" fn lower_aarch64_synchronous(e: &mut ExceptionContext) { + default_exception_handler(e); +} + +#[unsafe(no_mangle)] +extern "C" fn lower_aarch64_irq(e: &mut ExceptionContext) { + default_exception_handler(e); +} + +#[unsafe(no_mangle)] +extern "C" fn lower_aarch64_serror(e: &mut ExceptionContext) { + default_exception_handler(e); +} + +//------------------------------------------------------------------------------ +// Lower, AArch32 +//------------------------------------------------------------------------------ + +#[unsafe(no_mangle)] +extern "C" fn lower_aarch32_synchronous(e: &mut ExceptionContext) { + default_exception_handler(e); +} + +#[unsafe(no_mangle)] +extern "C" fn lower_aarch32_irq(e: &mut ExceptionContext) { + default_exception_handler(e); +} + +#[unsafe(no_mangle)] +extern "C" fn lower_aarch32_serror(e: &mut ExceptionContext) { + default_exception_handler(e); +} + +//-------------------------------------------------------------------------------------------------- +// Public Code +//-------------------------------------------------------------------------------------------------- + +/// The processor's current privilege level. +pub fn current_privilege_level() -> (PrivilegeLevel, &'static str) { + let el = CurrentEL.read_as_enum(CurrentEL::EL); + match el { + Some(CurrentEL::EL::Value::EL3) => (PrivilegeLevel::Unknown, "EL3"), + Some(CurrentEL::EL::Value::EL2) => (PrivilegeLevel::Hypervisor, "EL2"), + Some(CurrentEL::EL::Value::EL1) => (PrivilegeLevel::Kernel, "EL1"), + Some(CurrentEL::EL::Value::EL0) => (PrivilegeLevel::User, "EL0"), + _ => (PrivilegeLevel::Unknown, "Unknown"), + } +} + +/// Init exception handling by setting the exception vector base address register. +/// +/// - Changes the HW state of the executing core. +/// - The vector table and the symbol `__EXCEPTION_VECTORS_START` from the linker script must +/// adhere to the alignment and size constraints demanded by the ARMv8-A Architecture Reference +/// Manual. +pub fn handling_init() { + unsafe extern "Rust" { + static __EXCEPTION_VECTORS_START: UnsafeCell<()>; + } + + // Safety: __EXCEPTION_VECTORS_START is a valid pointer to exception vector table + // that is properly aligned and accessible during system initialization + unsafe { + set_vbar_el1_checked(__EXCEPTION_VECTORS_START.get() as u64) + .expect("Vector table properly aligned!"); + } + info!("[!] Exception traps set up"); +} + +/// Errors possibly returned from the traps module. +/// @todo a big over-engineered here. +#[derive(Debug, Snafu)] +enum Error { + /// IVT address is unaligned. + #[snafu(display("Unaligned base address for interrupt vector table"))] + Unaligned, +} + +/// Configure base address of interrupt vectors table. +/// Checks that address is properly 2KiB aligned. +/// +/// # Safety +/// +/// Totally unsafe in the land of the hardware. +unsafe fn set_vbar_el1_checked(vec_base_addr: u64) -> Result<(), Error> { + if vec_base_addr.trailing_zeros() < 11 { + return Err(Error::Unaligned); + } + + VBAR_EL1.set(vec_base_addr); + + // Force VBAR update to complete before next instruction. + barrier::isb(barrier::SY); + + Ok(()) +} diff --git a/libs/exception/src/arch/aarch64/spsr_el1.rs b/libs/exception/src/arch/aarch64/spsr_el1.rs new file mode 100644 index 000000000..5ce31a31c --- /dev/null +++ b/libs/exception/src/arch/aarch64/spsr_el1.rs @@ -0,0 +1,37 @@ +use {aarch64_cpu::registers::SPSR_EL1, core::fmt, tock_registers::LocalRegisterCopy}; + +#[repr(transparent)] +pub struct SpsrEL1(pub LocalRegisterCopy); + +/// Human readable `SPSR_EL1`. +#[rustfmt::skip] +impl fmt::Display for SpsrEL1 { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + // Raw value. + writeln!(f, "SPSR_EL1: {:#010x}", self.0.get())?; + + let to_flag_str = |x| -> _ { + if x { "Set" } else { "Not set" } + }; + + writeln!(f, " Flags:")?; + writeln!(f, " Negative (N): {}", to_flag_str(self.0.is_set(SPSR_EL1::N)))?; + writeln!(f, " Zero (Z): {}", to_flag_str(self.0.is_set(SPSR_EL1::Z)))?; + writeln!(f, " Carry (C): {}", to_flag_str(self.0.is_set(SPSR_EL1::C)))?; + writeln!(f, " Overflow (V): {}", to_flag_str(self.0.is_set(SPSR_EL1::V)))?; + + let to_mask_str = |x| -> _ { + if x { "Masked" } else { "Unmasked" } + }; + + writeln!(f, " Exception handling state:")?; + writeln!(f, " Debug (D): {}", to_mask_str(self.0.is_set(SPSR_EL1::D)))?; + writeln!(f, " SError (A): {}", to_mask_str(self.0.is_set(SPSR_EL1::A)))?; + writeln!(f, " IRQ (I): {}", to_mask_str(self.0.is_set(SPSR_EL1::I)))?; + writeln!(f, " FIQ (F): {}", to_mask_str(self.0.is_set(SPSR_EL1::F)))?; + + write!(f, " Illegal Execution State (IL): {}", + to_flag_str(self.0.is_set(SPSR_EL1::IL)) + ) + } +} diff --git a/libs/exception/src/arch/aarch64/exception/traps.rs b/libs/exception/src/arch/aarch64/traps.rs similarity index 100% rename from libs/exception/src/arch/aarch64/exception/traps.rs rename to libs/exception/src/arch/aarch64/traps.rs diff --git a/libs/exception/src/arch/aarch64/exception/vectors.S b/libs/exception/src/arch/aarch64/vectors.S similarity index 100% rename from libs/exception/src/arch/aarch64/exception/vectors.S rename to libs/exception/src/arch/aarch64/vectors.S diff --git a/libs/exception/src/arch/mod.rs b/libs/exception/src/arch/mod.rs index de0a2a6ad..1f57abe47 100644 --- a/libs/exception/src/arch/mod.rs +++ b/libs/exception/src/arch/mod.rs @@ -1 +1,2 @@ pub mod aarch64; +pub use aarch64::*; diff --git a/libs/exception/src/exception/asynchronous/mod.rs b/libs/exception/src/asynchronous/mod.rs similarity index 100% rename from libs/exception/src/exception/asynchronous/mod.rs rename to libs/exception/src/asynchronous/mod.rs diff --git a/libs/exception/src/exception/mod.rs b/libs/exception/src/exception/mod.rs deleted file mode 100644 index be67d6834..000000000 --- a/libs/exception/src/exception/mod.rs +++ /dev/null @@ -1,29 +0,0 @@ -// SPDX-License-Identifier: MIT OR Apache-2.0 -// -// Copyright (c) 2020-2022 Andre Richter - -//! Synchronous and asynchronous exception handling. - -#[cfg(target_arch = "aarch64")] -use crate::arch::aarch64::exception as arch_exception; - -pub mod asynchronous; - -//-------------------------------------------------------------------------------------------------- -// Architectural Public Reexports -//-------------------------------------------------------------------------------------------------- -pub use arch_exception::{current_privilege_level, handling_init}; - -//-------------------------------------------------------------------------------------------------- -// Public Definitions -//-------------------------------------------------------------------------------------------------- - -/// Kernel privilege levels. -#[allow(missing_docs)] -#[derive(Eq, PartialEq)] -pub enum PrivilegeLevel { - User, - Kernel, - Hypervisor, - Unknown, -} diff --git a/libs/exception/src/lib.rs b/libs/exception/src/lib.rs index 7e1046f03..bc75799c3 100644 --- a/libs/exception/src/lib.rs +++ b/libs/exception/src/lib.rs @@ -1,7 +1,35 @@ +// SPDX-License-Identifier: MIT OR Apache-2.0 +// +// Copyright (c) 2020-2022 Andre Richter + +//! Synchronous and asynchronous exception handling. + #![no_std] #![no_main] #![feature(format_args_nl)] #![feature(stmt_expr_attributes)] pub mod arch; -pub mod exception; +pub mod asynchronous; + +#[cfg(target_arch = "aarch64")] +use crate::arch::aarch64 as arch_exception; + +//-------------------------------------------------------------------------------------------------- +// Architectural Public Reexports +//-------------------------------------------------------------------------------------------------- +pub use arch_exception::{current_privilege_level, handling_init}; + +//-------------------------------------------------------------------------------------------------- +// Public Definitions +//-------------------------------------------------------------------------------------------------- + +/// Kernel privilege levels. +#[allow(missing_docs)] +#[derive(Eq, PartialEq)] +pub enum PrivilegeLevel { + User, + Kernel, + Hypervisor, + Unknown, +} diff --git a/libs/object/src/debug_console.rs b/libs/object/src/debug_console.rs index c663e2e6d..9ae5259a1 100644 --- a/libs/object/src/debug_console.rs +++ b/libs/object/src/debug_console.rs @@ -51,7 +51,7 @@ impl DebugConsoleKey { ) }; libqemu::semi_println!( - "Return from DebugConsoleOp::Write with result ({}, {}, {})", + "Userspace return from DebugConsoleOp::Write with result ({}, {}, {})", ok, r1, r2 diff --git a/libs/platform/src/platform/raspberrypi/device_driver/bcm/interrupt_controller/mod.rs b/libs/platform/src/platform/raspberrypi/device_driver/bcm/interrupt_controller/mod.rs index 20cc1535b..571eca55d 100644 --- a/libs/platform/src/platform/raspberrypi/device_driver/bcm/interrupt_controller/mod.rs +++ b/libs/platform/src/platform/raspberrypi/device_driver/bcm/interrupt_controller/mod.rs @@ -8,7 +8,7 @@ mod peripheral_ic; use { core::fmt, - libexception::exception::{self, asynchronous::IRQHandlerDescriptor}, + libexception::asynchronous::{IRQHandlerDescriptor, interface::IRQManager}, libmemory::{Address, Virtual}, libprimitives::BoundedUsize, }; @@ -111,12 +111,12 @@ impl libdriver::drivers::interface::DeviceDriver for InterruptController { } } -impl exception::asynchronous::interface::IRQManager for InterruptController { +impl IRQManager for InterruptController { type IRQNumberType = IRQNumber; fn register_handler( &self, - irq_handler_descriptor: exception::asynchronous::IRQHandlerDescriptor, + irq_handler_descriptor: IRQHandlerDescriptor, ) -> Result<(), &'static str> { match irq_handler_descriptor.number() { IRQNumber::Local(_) => unimplemented!("Local IRQ controller not implemented."), @@ -142,7 +142,7 @@ impl exception::asynchronous::interface::IRQManager for InterruptController { fn handle_pending_irqs<'irq_context>( &'irq_context self, - ic: &exception::asynchronous::IRQContext<'irq_context>, + ic: &libexception::asynchronous::IRQContext<'irq_context>, ) { // It can only be a peripheral IRQ pending because enable() does not support local IRQs yet. self.periph.handle_pending_irqs(ic); diff --git a/libs/platform/src/platform/raspberrypi/device_driver/bcm/interrupt_controller/peripheral_ic.rs b/libs/platform/src/platform/raspberrypi/device_driver/bcm/interrupt_controller/peripheral_ic.rs index 1c1cf70a9..bf70b3d52 100644 --- a/libs/platform/src/platform/raspberrypi/device_driver/bcm/interrupt_controller/peripheral_ic.rs +++ b/libs/platform/src/platform/raspberrypi/device_driver/bcm/interrupt_controller/peripheral_ic.rs @@ -11,9 +11,7 @@ use { super::{PendingIRQs, PeripheralIRQ}, crate::platform::device_driver::common::MMIODerefWrapper, - libexception::exception::asynchronous::{ - IRQContext, IRQHandlerDescriptor, interface::IRQManager, - }, + libexception::asynchronous::{IRQContext, IRQHandlerDescriptor, interface::IRQManager}, liblocking::{self, IRQSafeNullLock, InitStateLock}, tock_registers::{ interfaces::{Readable, Writeable}, diff --git a/libs/platform/src/platform/raspberrypi/device_driver/bcm/pl011_uart.rs b/libs/platform/src/platform/raspberrypi/device_driver/bcm/pl011_uart.rs index 58d1befc6..8341df6a6 100644 --- a/libs/platform/src/platform/raspberrypi/device_driver/bcm/pl011_uart.rs +++ b/libs/platform/src/platform/raspberrypi/device_driver/bcm/pl011_uart.rs @@ -521,7 +521,7 @@ impl libdriver::drivers::interface::DeviceDriver for PL011Uart { ) -> Result<(), &'static str> { use { crate::platform::exception::asynchronous::irq_manager, - libexception::exception::asynchronous::IRQHandlerDescriptor, + libexception::asynchronous::IRQHandlerDescriptor, }; let descriptor = IRQHandlerDescriptor::new(irq_number, Self::COMPATIBLE, self); @@ -567,7 +567,7 @@ impl interface::ConsoleOps for PL011Uart { impl interface::All for PL011Uart {} -impl libexception::exception::asynchronous::interface::IRQHandler for PL011Uart { +impl libexception::asynchronous::interface::IRQHandler for PL011Uart { fn handle(&self) -> Result<(), &'static str> { self.inner.lock(|inner| { let pending = inner.registers.MaskedInterruptStatus.extract(); diff --git a/libs/platform/src/platform/raspberrypi/exception/asynchronous.rs b/libs/platform/src/platform/raspberrypi/exception/asynchronous.rs index 73c7201ee..24e795a28 100644 --- a/libs/platform/src/platform/raspberrypi/exception/asynchronous.rs +++ b/libs/platform/src/platform/raspberrypi/exception/asynchronous.rs @@ -5,9 +5,7 @@ //! Platform asynchronous exception handling. use { - libexception::exception::asynchronous::{ - IRQContext, IRQHandlerDescriptor, interface::IRQManager, - }, + libexception::asynchronous::{IRQContext, IRQHandlerDescriptor, interface::IRQManager}, liblocking::InitStateLock, }; From 3101b308d9abe2991142fed449271b6972bb6868 Mon Sep 17 00:00:00 2001 From: Berkus Decker Date: Fri, 6 Feb 2026 11:29:07 +0200 Subject: [PATCH 062/107] wip: update plan progress --- kernel/Plan.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/kernel/Plan.md b/kernel/Plan.md index 0dcbc0437..456869571 100644 --- a/kernel/Plan.md +++ b/kernel/Plan.md @@ -15,9 +15,9 @@ Steps: - [x] Print DTB - [x] Print max RAM from DTB -- [ ] Allocate a single key slot in a global domain struct -- [ ] Fill it with capability to DebugConsole -- [ ] Invoke DebugConsole.Write via syscall +- [x] Allocate a single key slot in a global domain struct +- [x] Fill it with capability to DebugConsole +- [x] Invoke DebugConsole.Write via syscall - [ ] Print kernel covered area - [ ] Print KERNEL_HIGH_BASE From bff2aa9b1092a663af4d62cf74799e15c0cd7e0f Mon Sep 17 00:00:00 2001 From: Berkus Decker Date: Fri, 6 Feb 2026 11:30:32 +0200 Subject: [PATCH 063/107] wip: clean up exceptions code --- kernel/init_thread/build.rs | 5 +- kernel/init_thread/src/el_switch.rs | 11 +- kernel/nucleus/_build.rs | 6 - kernel/nucleus/nucleus.ld | 133 ------------ kernel/nucleus/src/main.rs | 197 +++++++++++------- kernel/nucleus/src/vectors.rs | 121 ----------- .../src/arch/aarch64/{traps.rs => debug.rs} | 9 +- libs/exception/src/arch/aarch64/mod.rs | 179 +--------------- libs/exception/src/arch/aarch64/vectors.S | 26 ++- libs/exception/src/lib.rs | 2 +- .../raspberrypi/linker}/init_thread.ld | 0 .../src/platform/raspberrypi/linker/kernel.ld | 167 --------------- .../raspberrypi/linker/nucleus-new.ld | 53 ----- .../platform/raspberrypi/linker/nucleus.ld | 95 +++++++++ 14 files changed, 265 insertions(+), 739 deletions(-) delete mode 100644 kernel/nucleus/nucleus.ld delete mode 100644 kernel/nucleus/src/vectors.rs rename libs/exception/src/arch/aarch64/{traps.rs => debug.rs} (97%) rename {kernel/init_thread => libs/platform/src/platform/raspberrypi/linker}/init_thread.ld (100%) delete mode 100644 libs/platform/src/platform/raspberrypi/linker/kernel.ld delete mode 100644 libs/platform/src/platform/raspberrypi/linker/nucleus-new.ld create mode 100644 libs/platform/src/platform/raspberrypi/linker/nucleus.ld diff --git a/kernel/init_thread/build.rs b/kernel/init_thread/build.rs index a01a6562d..a7e3dd072 100644 --- a/kernel/init_thread/build.rs +++ b/kernel/init_thread/build.rs @@ -18,9 +18,6 @@ fn main() { output::rerun_if_changed(kernel_elf_path); output::rerun_if_changed("build.rs"); output::rerun_if_changed("kernel_sections.template.rs"); - // output::rustc_link_arg( - // format!("--script={}/init_thread.ld", env!("CARGO_MANIFEST_DIR")).as_ref(), - // ); let out_dir = env::var("OUT_DIR").unwrap(); let out_path = Path::new(&out_dir); @@ -235,7 +232,7 @@ fn find_symbol(elf: &Elf, symbol_name: &str) -> Option { /// Try to find vector table location from symbols fn find_vector_table_from_symbols(elf: &Elf) -> Option { // Common symbol names for exception vectors - const VECTOR_SYMBOL: &str = "__vectors"; + const VECTOR_SYMBOL: &str = "__exception_vectors_start"; // From libexception::arch if let Some(sym) = find_symbol(elf, VECTOR_SYMBOL) { info!( diff --git a/kernel/init_thread/src/el_switch.rs b/kernel/init_thread/src/el_switch.rs index e23e5ee26..29ecb3c07 100644 --- a/kernel/init_thread/src/el_switch.rs +++ b/kernel/init_thread/src/el_switch.rs @@ -3,7 +3,7 @@ use { crate::loader::memory_barrier, aarch64_cpu::{ - asm, + asm::{self, barrier}, registers::{ ELR_EL2, HCR_EL2, MAIR_EL1, ReadWriteable, SCTLR_EL1, SP_EL1, SPSR_EL1, SPSR_EL2, TCR_EL1, TTBR0_EL1, TTBR1_EL1, VBAR_EL1, Writeable, @@ -51,12 +51,21 @@ pub unsafe fn enable_mmu_and_drop_to_el1( // STEP 2: Set up VBAR_EL1 (Exception Vector Base Address) // ═══════════════════════════════════════════════════════════ + if vbar.trailing_zeros() < 11 { + panic!("Vector table NOT properly aligned!"); + } + // The address must be 2KB aligned (bits [10:0] must be 0). // We set the virtual address here since VBAR_EL1 is only // used after MMU is enabled (exceptions before ERET would // be taken at EL2, not EL1). VBAR_EL1.set(vbar); + // Force VBAR update to complete before next instruction. + barrier::isb(barrier::SY); + + liblog::info!("[!] Exception traps set up"); + // ═══════════════════════════════════════════════════════════ // STEP 3: Configure EL1 MMU settings // ═══════════════════════════════════════════════════════════ diff --git a/kernel/nucleus/_build.rs b/kernel/nucleus/_build.rs index c3db5d2f7..9c07082d1 100644 --- a/kernel/nucleus/_build.rs +++ b/kernel/nucleus/_build.rs @@ -1,11 +1,5 @@ //! This build script is used to link main kernel binary. -fn main() { - // build_rs::output::rustc_link_arg( - // format!("--script={}/nucleus.ld", env!("CARGO_MANIFEST_DIR")).as_ref(), - // ); -} - // const LINKER_SCRIPT: &str = "libs/platform/src/platform/raspberrypi/linker/kernel.ld"; // const LINKER_SCRIPT_AUX: &str = "libs/exception/src/arch/aarch64/linker/aarch64-exceptions.ld"; diff --git a/kernel/nucleus/nucleus.ld b/kernel/nucleus/nucleus.ld deleted file mode 100644 index 4ad2f4548..000000000 --- a/kernel/nucleus/nucleus.ld +++ /dev/null @@ -1,133 +0,0 @@ -/* - * SPDX-License-Identifier: MIT OR BlueOak-1.0.0 - * Copyright (c) 2018 Andre Richter - * Copyright (c) Berkus Decker - * Original code distributed under MIT, additional changes are under BlueOak-1.0.0 - */ - -__PAGE_SIZE = 64K; -__PAGE_MASK = __PAGE_SIZE - 1; - -__KERNEL_VIRT_ADDR_BASE = 0xffff800000000000; /* Nucleus is mapped here, init_thread is identity-mapped */ - -/* Flags: - * 4 == R - * 5 == RX - * 6 == RW - * - * Segments are marked PT_LOAD below so that the ELF file provides virtual and physical addresses. - * It doesn't mean all of them need actually be loaded. - */ -PHDRS -{ - segment_code PT_LOAD FLAGS(5); - segment_data PT_LOAD FLAGS(6); -} - -SECTIONS -{ - . = __KERNEL_VIRT_ADDR_BASE; - - /* __nucleus_start = .; */ - - .text : /*AT(ADDR(.text) - __KERNEL_VIRT_ADDR_BASE)*/ ALIGN(__PAGE_SIZE) - { - /******************************************************************************************* - * Regular Kernel Code - *******************************************************************************************/ - *(.text*) - . = ALIGN(2K); - KEEP(*(.vectors)) - . = ALIGN(2K); - } :segment_code - - /* TODO: rodata is currently executable, but doesn't have to... also each individual section must be aligned to 4K at least for mappings to work */ - .rodata : /*AT(ADDR(.rodata) - __KERNEL_VIRT_ADDR_BASE)*/ ALIGN(__PAGE_SIZE) - { - *(.rodata*) - FILL(0x00) - . = ALIGN(__PAGE_SIZE); /* Fill up to page size */ - } :segment_code - - /* __CODE_END = .; - ASSERT((__CODE_END & __PAGE_MASK) == 0, "End of kernel code is not page aligned") */ - - /*********************************************************************************************** - * Data + BSS - ***********************************************************************************************/ - - .data : /*AT(ADDR(.data) - __KERNEL_VIRT_ADDR_BASE)*/ ALIGN(__PAGE_SIZE) - { - /* __DATA_START = .; - ASSERT((__DATA_START & __PAGE_MASK) == 0, "Start of kernel data is not page aligned") */ - - *(.data*) - FILL(0x00) - - . = ALIGN(__PAGE_SIZE); - } :segment_data - - .bss (NOLOAD) : /*AT(ADDR(.bss) - __KERNEL_VIRT_ADDR_BASE)*/ ALIGN(__PAGE_SIZE) - { - /* __BSS_START = LOADADDR(.bss); LMA (PhysAddr) - for the loader in init_thread */ - *(.bss*) - *(COMMON) - . = ALIGN(__PAGE_SIZE); /* Align up to page size */ - } :segment_data - - . += __PAGE_SIZE; /* add guard page between BSS and stack! */ - - __STACK_VIRT_BOTTOM = .; - - /* __BSS_END = __BSS_START + ALIGN(SIZEOF(.bss), __PAGE_SIZE); *//* LMA, Physical address */ - /* __DATA_END = .; *//* VMA, Virtual address */ - - /*********************************************************************************************** - * MMIO Remap Reserved (8Mb) - ***********************************************************************************************/ - /* __MMIO_REMAP_START = .; FIXME: VMA? Must be LMA perhaps, the point is to remap this - . += 8 * 1024 * 1024; - __MMIO_REMAP_END = .; FIXME: VMA? Must be LMA perhaps, the point is to remap this */ - - /* ASSERT((. & __PAGE_MASK) == 0, "MMIO remap reservation is not page aligned") - __kernel_virt_addr_space_size = ABSOLUTE(. - __KERNEL_VIRT_ADDR_BASE); */ - - /* Physical end */ - /* __nucleus_end = __nucleus_start + __kernel_virt_addr_space_size; - __kernel_end = .; */ - - /*********************************************************************************************** - * Misc - ***********************************************************************************************/ - - .got : { *(.got*) } - ASSERT(SIZEOF(.got) == 0, "Unexpected relocations in kernel binary.") - - /DISCARD/ : { - *(.comment*) - *(.gnu*) - *(.note*) - *(.eh_frame*) - *(.text.chainboot*) - } -} - -PROVIDE(current_el0_synchronous = current_el0_synchronous); -PROVIDE(current_el0_irq = current_el0_irq); -PROVIDE(current_el0_fiq = default_exception_handler); -PROVIDE(current_el0_serror = current_el0_serror); - -PROVIDE(current_elx_synchronous = current_elx_synchronous); -PROVIDE(current_elx_irq = current_elx_irq); -PROVIDE(current_elx_fiq = default_exception_handler); -PROVIDE(current_elx_serror = current_elx_serror); - -PROVIDE(lower_aarch64_synchronous = lower_aarch64_synchronous); -PROVIDE(lower_aarch64_irq = lower_aarch64_irq); -PROVIDE(lower_aarch64_fiq = default_exception_handler); -PROVIDE(lower_aarch64_serror = lower_aarch64_serror); - -PROVIDE(lower_aarch32_synchronous = lower_aarch32_synchronous); -PROVIDE(lower_aarch32_irq = lower_aarch32_irq); -PROVIDE(lower_aarch32_fiq = default_exception_handler); -PROVIDE(lower_aarch32_serror = lower_aarch32_serror); diff --git a/kernel/nucleus/src/main.rs b/kernel/nucleus/src/main.rs index 990446daf..44bbd5a83 100644 --- a/kernel/nucleus/src/main.rs +++ b/kernel/nucleus/src/main.rs @@ -44,8 +44,6 @@ use { mod api; /// Nucleus objects implementations mod objects; -/// Exception vectors triggering syscall handing and general IRQ routing -mod vectors; // TODO: Split this into read-only part, that does not need locks, per-cpu mutable part that does not need locks, // TODO: Shared atomic counters that do not need locks and shared mutable collections that DO need locks (but should be minority) @@ -69,82 +67,135 @@ fn panicked(info: &PanicInfo) -> ! { libmachine::panic::handler(info) } -// Syscall handler - exception vector for EL0 synchronous exceptions -// (the only other thing nucleus does) -#[unsafe(naked)] +//------------------------------------------------------------------------------ +//------------------------------------------------------------------------------ +// Exception handlers +//------------------------------------------------------------------------------ +//------------------------------------------------------------------------------ + +/// The default exception handler, invoked for every exception type unless the handler +/// is overridden. +/// Prints verbose information about the exception and then panics. +/// +/// Default pointer is configured in the linker script. #[unsafe(no_mangle)] -unsafe extern "C" fn syscall_handler() { - core::arch::naked_asm!( - // Save user context to kernel stack - "sub sp, sp, #16 * 17", - "", - "stp x0, x1, [sp, #16 * 0]", - "stp x2, x3, [sp, #16 * 1]", - "stp x4, x5, [sp, #16 * 2]", - "stp x6, x7, [sp, #16 * 3]", - "stp x8, x9, [sp, #16 * 4]", - "stp x10, x11, [sp, #16 * 5]", - "stp x12, x13, [sp, #16 * 6]", - "stp x14, x15, [sp, #16 * 7]", - "stp x16, x17, [sp, #16 * 8]", - "stp x18, x19, [sp, #16 * 9]", - "stp x20, x21, [sp, #16 * 10]", - "stp x22, x23, [sp, #16 * 11]", - "stp x24, x25, [sp, #16 * 12]", - "stp x26, x27, [sp, #16 * 13]", - "stp x28, x29, [sp, #16 * 14]", - "", - "mrs x10, SPSR_EL1", - "mrs x11, ELR_EL1", - "", - "stp x30, x10, [sp, #16 * 15]", - "str x11, [sp, #16 * 16]", - // x0-x7 already in place for Rust function call - "mov x0, sp", // register frame pointer for handler - "bl cap_invoke_handler", - "", - // Return values in x0, x1, x2 are set in the trap frame - "ldr x19, [sp, #16 * 16]", - "ldp x30, x20, [sp, #16 * 15]", - "", - "msr ELR_EL1, x19", - "msr SPSR_EL1, x20", - "", - "ldp x0, x1, [sp, #16 * 0]", - "ldp x2, x3, [sp, #16 * 1]", - "ldp x4, x5, [sp, #16 * 2]", - "ldp x6, x7, [sp, #16 * 3]", - "ldp x8, x9, [sp, #16 * 4]", - "ldp x10, x11, [sp, #16 * 5]", - "ldp x12, x13, [sp, #16 * 6]", - "ldp x14, x15, [sp, #16 * 7]", - "ldp x16, x17, [sp, #16 * 8]", - "ldp x18, x19, [sp, #16 * 9]", - "ldp x20, x21, [sp, #16 * 10]", - "ldp x22, x23, [sp, #16 * 11]", - "ldp x24, x25, [sp, #16 * 12]", - "ldp x26, x27, [sp, #16 * 13]", - "ldp x28, x29, [sp, #16 * 14]", - "", - "add sp, sp, #16 * 17", - "", - "eret", +extern "C" fn default_exception_handler(exc: &ExceptionContext) { + panic!( + "Unexpected CPU Exception!\n\n\ + {}", + exc ); } -/// Kernel entry point +//------------------------------------------------------------------------------ +// Current, EL0 +//------------------------------------------------------------------------------ + +#[unsafe(no_mangle)] +extern "C" fn current_el0_synchronous(_e: &mut ExceptionContext) { + panic!("Should not be here. Use of SP_EL0 in EL1 is not supported.") +} + +#[unsafe(no_mangle)] +extern "C" fn current_el0_irq(_e: &mut ExceptionContext) { + panic!("Should not be here. Use of SP_EL0 in EL1 is not supported.") +} + +#[unsafe(no_mangle)] +extern "C" fn current_el0_serror(_e: &mut ExceptionContext) { + panic!("Should not be here. Use of SP_EL0 in EL1 is not supported.") +} + +//------------------------------------------------------------------------------ +// Current, ELx +//------------------------------------------------------------------------------ + +#[cfg(not(any(test, feature = "test_build")))] +#[unsafe(no_mangle)] +extern "C" fn current_elx_synchronous(e: &mut ExceptionContext) { + cap_invoke_handler(e) +} + +#[cfg(any(test, feature = "test_build"))] +#[unsafe(no_mangle)] +extern "C" fn current_elx_synchronous(e: &mut ExceptionContext) { + { + const TEST_SVC_ID: u64 = 0x1337; + + let esr_el1 = esr_el1::EsrEL1(LocalRegisterCopy::new(ESR_EL1.get())); + + if let Some(ESR_EL1::EC::Value::SVC64) = esr_el1.exception_class() + && esr_el1.iss() == TEST_SVC_ID + { + liblog::println!("Serving syscall {TEST_SVC_ID}"); + return; + } + } + + if debug::exception_dump(e) { + return; + } + + default_exception_handler(e); +} + +#[unsafe(no_mangle)] +extern "C" fn current_elx_irq(e: &mut ExceptionContext) { + // -- @todo + // let token = unsafe { &exception::asynchronous::IRQContext::new() }; + // exception::asynchronous::irq_manager().handle_pending_irqs(token); + default_exception_handler(e); +} + +#[unsafe(no_mangle)] +extern "C" fn current_elx_serror(e: &mut ExceptionContext) { + default_exception_handler(e); +} + +//------------------------------------------------------------------------------ +// Lower, AArch64 +//------------------------------------------------------------------------------ + +#[unsafe(no_mangle)] +extern "C" fn lower_aarch64_synchronous(e: &mut ExceptionContext) { + cap_invoke_handler(e) +} + +#[unsafe(no_mangle)] +extern "C" fn lower_aarch64_irq(e: &mut ExceptionContext) { + default_exception_handler(e); +} + +#[unsafe(no_mangle)] +extern "C" fn lower_aarch64_serror(e: &mut ExceptionContext) { + default_exception_handler(e); +} + +//------------------------------------------------------------------------------ +// Lower, AArch32 +//------------------------------------------------------------------------------ + +#[unsafe(no_mangle)] +extern "C" fn lower_aarch32_synchronous(e: &mut ExceptionContext) { + default_exception_handler(e); +} + +#[unsafe(no_mangle)] +extern "C" fn lower_aarch32_irq(e: &mut ExceptionContext) { + default_exception_handler(e); +} + +#[unsafe(no_mangle)] +extern "C" fn lower_aarch32_serror(e: &mut ExceptionContext) { + default_exception_handler(e); +} + +//------------------------------------------------------------------------------ +// Kernel entry point +//------------------------------------------------------------------------------ + #[unsafe(no_mangle)] -fn cap_invoke_handler( - // cap_slot: u32, - // op: u32, - // arg0: u64, - // arg1: u64, - // arg2: u64, - // arg3: u64, - // arg4: u64, - // arg5: u64, - frame: &mut ExceptionContext, -) { +fn cap_invoke_handler(frame: &mut ExceptionContext) { let cap_slot = frame.gpr[0] as u32; let op = frame.gpr[1] as u32; semi_println!( diff --git a/kernel/nucleus/src/vectors.rs b/kernel/nucleus/src/vectors.rs deleted file mode 100644 index 181cd7d58..000000000 --- a/kernel/nucleus/src/vectors.rs +++ /dev/null @@ -1,121 +0,0 @@ -// Exception vector table for kernel - -use core::arch::global_asm; - -// Exception vector table must be 2KB aligned -// 16 entries × 128 bytes each = 2048 bytes total -// -// The table is organized as: -// - 4 entries for exceptions from current EL with SP_EL0 -// - 4 entries for exceptions from current EL with SP_ELx -// - 4 entries for exceptions from lower EL (AArch64) -// - 4 entries for exceptions from lower EL (AArch32) - -global_asm!( - r#" -.section .vectors, "ax" -.balign 2048 -.global __vectors - -__vectors: - // ═══════════════════════════════════════════════════════════════ - // Current EL with SP_EL0 (not used, we use SP_EL1) - // ═══════════════════════════════════════════════════════════════ - -.balign 128 -curr_el_sp0_sync: - b exception_handler_sync - -.balign 128 -curr_el_sp0_irq: - b exception_handler_irq - -.balign 128 -curr_el_sp0_fiq: - b exception_handler_fiq - -.balign 128 -curr_el_sp0_serror: - b exception_handler_serror - - // ═══════════════════════════════════════════════════════════════ - // Current EL with SP_ELx (kernel exceptions) - // ═══════════════════════════════════════════════════════════════ - -.balign 128 -curr_el_spx_sync: - b syscall_handler // SVC in EL1/EL2 - -.balign 128 -curr_el_spx_irq: - b exception_handler_irq - -.balign 128 -curr_el_spx_fiq: - b exception_handler_fiq - -.balign 128 -curr_el_spx_serror: - b exception_handler_serror - - // ═══════════════════════════════════════════════════════════════ - // Lower EL using AArch64 (user-space syscalls and exceptions) - // ═══════════════════════════════════════════════════════════════ - -.balign 128 -lower_el_aarch64_sync: - b syscall_handler // SVC from user space - -.balign 128 -lower_el_aarch64_irq: - b exception_handler_irq - -.balign 128 -lower_el_aarch64_fiq: - b exception_handler_fiq - -.balign 128 -lower_el_aarch64_serror: - b exception_handler_serror - - // ═══════════════════════════════════════════════════════════════ - // Lower EL using AArch32 (not supported) - // ═══════════════════════════════════════════════════════════════ - -.balign 128 -lower_el_aarch32_sync: - b exception_handler_unsupported - -.balign 128 -lower_el_aarch32_irq: - b exception_handler_unsupported - -.balign 128 -lower_el_aarch32_fiq: - b exception_handler_unsupported - -.balign 128 -lower_el_aarch32_serror: - b exception_handler_unsupported - -// ═══════════════════════════════════════════════════════════════ -// Exception handlers (stubs - implement properly in Rust) -// ═══════════════════════════════════════════════════════════════ - -exception_handler_sync: - // Save context, call Rust handler, restore context - b . - -exception_handler_irq: - b . - -exception_handler_fiq: - b . - -exception_handler_serror: - b . - -exception_handler_unsupported: - b . -"# -); diff --git a/libs/exception/src/arch/aarch64/traps.rs b/libs/exception/src/arch/aarch64/debug.rs similarity index 97% rename from libs/exception/src/arch/aarch64/traps.rs rename to libs/exception/src/arch/aarch64/debug.rs index 159113b44..949c0f7e2 100644 --- a/libs/exception/src/arch/aarch64/traps.rs +++ b/libs/exception/src/arch/aarch64/debug.rs @@ -7,7 +7,7 @@ use { tock_registers::{LocalRegisterCopy, interfaces::Readable, register_bitfields}, }; -fn cause_to_string(cause: u64) -> &'static str { +pub fn cause_to_string(cause: u64) -> &'static str { if cause == ESR_EL1::EC::DataAbortCurrentEL.read(ESR_EL1::EC) { "Data Alignment Check" } else { @@ -115,9 +115,9 @@ register_bitfields! { ] } -type IssForDataAbort = LocalRegisterCopy; +pub type IssForDataAbort = LocalRegisterCopy; -fn iss_dfsc_to_string(iss: IssForDataAbort) -> &'static str { +pub fn iss_dfsc_to_string(iss: IssForDataAbort) -> &'static str { match iss.read_as_enum(ISS_DA::DFSC) { Some(ISS_DA::DFSC::Value::AddressSizeTL0) => { "Address size fault, level 0 of translation or translation table base register" @@ -183,7 +183,8 @@ fn iss_dfsc_to_string(iss: IssForDataAbort) -> &'static str { /// Not for production use! /// @todo merge this with default exception handler a bit? /// @returns true if we can skip the instruction and return, false if we need to continue handling the exception. -pub fn synchronous_common(e: &mut super::ExceptionContext) -> bool { +// #[cfg(any(test, feature = "test_build"))] +pub fn exception_dump(e: &mut super::ExceptionContext) -> bool { println!(" ESR_EL1: {:#010x} (syndrome)", ESR_EL1.get()); let cause = ESR_EL1.read(ESR_EL1::EC); println!( diff --git a/libs/exception/src/arch/aarch64/mod.rs b/libs/exception/src/arch/aarch64/mod.rs index 4d6ef2fa6..710baf458 100644 --- a/libs/exception/src/arch/aarch64/mod.rs +++ b/libs/exception/src/arch/aarch64/mod.rs @@ -50,140 +50,21 @@ //! to explicitly re-enable interrupts use { - crate::PrivilegeLevel, - aarch64_cpu::{asm::barrier, registers::*}, - core::cell::UnsafeCell, - liblog::info, - snafu::Snafu, - tock_registers::interfaces::{Readable, Writeable}, + crate::PrivilegeLevel, aarch64_cpu::registers::CurrentEL, tock_registers::interfaces::Readable, }; +mod debug; mod esr_el1; mod exception_context; mod spsr_el1; -mod traps; pub use exception_context::ExceptionContext; -core::arch::global_asm!(include_str!("vectors.S")); - -//-------------------------------------------------------------------------------------------------- -// Private Definitions -//-------------------------------------------------------------------------------------------------- - -//-------------------------------------------------------------------------------------------------- -// Private Code -//-------------------------------------------------------------------------------------------------- - -/// The default exception, invoked for every exception type unless the handler -/// is overridden. -/// Prints verbose information about the exception and then panics. -/// -/// Default pointer is configured in the linker script. -#[unsafe(no_mangle)] -extern "C" fn default_exception_handler(exc: &ExceptionContext) { - panic!( - "Unexpected CPU Exception!\n\n\ - {}", - exc - ); -} - -//------------------------------------------------------------------------------ -// Current, EL0 -//------------------------------------------------------------------------------ - -#[unsafe(no_mangle)] -extern "C" fn current_el0_synchronous(_e: &mut ExceptionContext) { - panic!("Should not be here. Use of SP_EL0 in EL1 is not supported.") -} - -#[unsafe(no_mangle)] -extern "C" fn current_el0_irq(_e: &mut ExceptionContext) { - panic!("Should not be here. Use of SP_EL0 in EL1 is not supported.") -} - -#[unsafe(no_mangle)] -extern "C" fn current_el0_serror(_e: &mut ExceptionContext) { - panic!("Should not be here. Use of SP_EL0 in EL1 is not supported.") -} - -//------------------------------------------------------------------------------ -// Current, ELx -//------------------------------------------------------------------------------ - -#[unsafe(no_mangle)] -extern "C" fn current_elx_synchronous(e: &mut ExceptionContext) { - #[cfg(any(test, feature = "test_build"))] - { - const TEST_SVC_ID: u64 = 0x1337; - - let esr_el1 = esr_el1::EsrEL1(LocalRegisterCopy::new(ESR_EL1.get())); - - if let Some(ESR_EL1::EC::Value::SVC64) = esr_el1.exception_class() - && esr_el1.iss() == TEST_SVC_ID - { - liblog::println!("Serving syscall {TEST_SVC_ID}"); - return; - } - } - - if traps::synchronous_common(e) { - return; - } - - default_exception_handler(e); -} - -#[unsafe(no_mangle)] -extern "C" fn current_elx_irq(_e: &mut ExceptionContext) { - // -- @todo - // let token = unsafe { &exception::asynchronous::IRQContext::new() }; - // exception::asynchronous::irq_manager().handle_pending_irqs(token); -} - -#[unsafe(no_mangle)] -extern "C" fn current_elx_serror(e: &mut ExceptionContext) { - default_exception_handler(e); -} - -//------------------------------------------------------------------------------ -// Lower, AArch64 -//------------------------------------------------------------------------------ - -#[unsafe(no_mangle)] -extern "C" fn lower_aarch64_synchronous(e: &mut ExceptionContext) { - default_exception_handler(e); -} - -#[unsafe(no_mangle)] -extern "C" fn lower_aarch64_irq(e: &mut ExceptionContext) { - default_exception_handler(e); -} - -#[unsafe(no_mangle)] -extern "C" fn lower_aarch64_serror(e: &mut ExceptionContext) { - default_exception_handler(e); -} - -//------------------------------------------------------------------------------ -// Lower, AArch32 -//------------------------------------------------------------------------------ - -#[unsafe(no_mangle)] -extern "C" fn lower_aarch32_synchronous(e: &mut ExceptionContext) { - default_exception_handler(e); -} +// Helpers for exception debugging +pub use debug::{IssForDataAbort, cause_to_string, exception_dump, iss_dfsc_to_string}; -#[unsafe(no_mangle)] -extern "C" fn lower_aarch32_irq(e: &mut ExceptionContext) { - default_exception_handler(e); -} - -#[unsafe(no_mangle)] -extern "C" fn lower_aarch32_serror(e: &mut ExceptionContext) { - default_exception_handler(e); -} +// Exception vectors for syscall handing and general IRQ routing +core::arch::global_asm!(include_str!("vectors.S")); //-------------------------------------------------------------------------------------------------- // Public Code @@ -200,51 +81,3 @@ pub fn current_privilege_level() -> (PrivilegeLevel, &'static str) { _ => (PrivilegeLevel::Unknown, "Unknown"), } } - -/// Init exception handling by setting the exception vector base address register. -/// -/// - Changes the HW state of the executing core. -/// - The vector table and the symbol `__EXCEPTION_VECTORS_START` from the linker script must -/// adhere to the alignment and size constraints demanded by the ARMv8-A Architecture Reference -/// Manual. -pub fn handling_init() { - unsafe extern "Rust" { - static __EXCEPTION_VECTORS_START: UnsafeCell<()>; - } - - // Safety: __EXCEPTION_VECTORS_START is a valid pointer to exception vector table - // that is properly aligned and accessible during system initialization - unsafe { - set_vbar_el1_checked(__EXCEPTION_VECTORS_START.get() as u64) - .expect("Vector table properly aligned!"); - } - info!("[!] Exception traps set up"); -} - -/// Errors possibly returned from the traps module. -/// @todo a big over-engineered here. -#[derive(Debug, Snafu)] -enum Error { - /// IVT address is unaligned. - #[snafu(display("Unaligned base address for interrupt vector table"))] - Unaligned, -} - -/// Configure base address of interrupt vectors table. -/// Checks that address is properly 2KiB aligned. -/// -/// # Safety -/// -/// Totally unsafe in the land of the hardware. -unsafe fn set_vbar_el1_checked(vec_base_addr: u64) -> Result<(), Error> { - if vec_base_addr.trailing_zeros() < 11 { - return Err(Error::Unaligned); - } - - VBAR_EL1.set(vec_base_addr); - - // Force VBAR update to complete before next instruction. - barrier::isb(barrier::SY); - - Ok(()) -} diff --git a/libs/exception/src/arch/aarch64/vectors.S b/libs/exception/src/arch/aarch64/vectors.S index 4d8959559..22aa709b6 100644 --- a/libs/exception/src/arch/aarch64/vectors.S +++ b/libs/exception/src/arch/aarch64/vectors.S @@ -43,25 +43,45 @@ b 1b .endm -// The vector definitions +// Exception vector table must be 2KB aligned +// 16 entries × 128 bytes each = 2048 bytes total +// +// The table is organized as: +// - 4 entries for exceptions from current EL with SP_EL0 +// - 4 entries for exceptions from current EL with SP_ELx +// - 4 entries for exceptions from lower EL (AArch64) +// - 4 entries for exceptions from lower EL (AArch32) .section .vectors, "ax" +.balign 2048 .global __exception_vectors_start __exception_vectors_start: + // ═══════════════════════════════════════════════════════════════ + // Current EL with SP_EL0 (not used, we use SP_EL1) + // ═══════════════════════════════════════════════════════════════ SAVE_CONTEXT_CALL_HANDLER_AND_RESTORE current_el0_synchronous // 0x000 SAVE_CONTEXT_CALL_HANDLER_AND_RESTORE current_el0_irq // 0x080 FIQ_DUMMY // 0x100 SAVE_CONTEXT_CALL_HANDLER_AND_RESTORE current_el0_serror // 0x180 - SAVE_CONTEXT_CALL_HANDLER_AND_RESTORE current_elx_synchronous // 0x200 + // ═══════════════════════════════════════════════════════════════ + // Current EL with SP_ELx (kernel exceptions) + // ═══════════════════════════════════════════════════════════════ + SAVE_CONTEXT_CALL_HANDLER_AND_RESTORE current_elx_synchronous // 0x200 - SVC syscall in EL1/EL2 SAVE_CONTEXT_CALL_HANDLER_AND_RESTORE current_elx_irq // 0x280 FIQ_DUMMY // 0x300 SAVE_CONTEXT_CALL_HANDLER_AND_RESTORE current_elx_serror // 0x380 - SAVE_CONTEXT_CALL_HANDLER_AND_RESTORE lower_aarch64_synchronous // 0x400 + // ═══════════════════════════════════════════════════════════════ + // Lower EL using AArch64 (user-space syscalls and exceptions) + // ═══════════════════════════════════════════════════════════════ + SAVE_CONTEXT_CALL_HANDLER_AND_RESTORE lower_aarch64_synchronous // 0x400 - SVC syscall from user space SAVE_CONTEXT_CALL_HANDLER_AND_RESTORE lower_aarch64_irq // 0x480 FIQ_DUMMY // 0x500 SAVE_CONTEXT_CALL_HANDLER_AND_RESTORE lower_aarch64_serror // 0x580 + // ═══════════════════════════════════════════════════════════════ + // Lower EL using AArch32 (not supported) + // ═══════════════════════════════════════════════════════════════ SAVE_CONTEXT_CALL_HANDLER_AND_RESTORE lower_aarch32_synchronous // 0x600 SAVE_CONTEXT_CALL_HANDLER_AND_RESTORE lower_aarch32_irq // 0x680 FIQ_DUMMY // 0x700 diff --git a/libs/exception/src/lib.rs b/libs/exception/src/lib.rs index bc75799c3..9672e6045 100644 --- a/libs/exception/src/lib.rs +++ b/libs/exception/src/lib.rs @@ -18,7 +18,7 @@ use crate::arch::aarch64 as arch_exception; //-------------------------------------------------------------------------------------------------- // Architectural Public Reexports //-------------------------------------------------------------------------------------------------- -pub use arch_exception::{current_privilege_level, handling_init}; +pub use arch_exception::current_privilege_level; //-------------------------------------------------------------------------------------------------- // Public Definitions diff --git a/kernel/init_thread/init_thread.ld b/libs/platform/src/platform/raspberrypi/linker/init_thread.ld similarity index 100% rename from kernel/init_thread/init_thread.ld rename to libs/platform/src/platform/raspberrypi/linker/init_thread.ld diff --git a/libs/platform/src/platform/raspberrypi/linker/kernel.ld b/libs/platform/src/platform/raspberrypi/linker/kernel.ld deleted file mode 100644 index 780374cbf..000000000 --- a/libs/platform/src/platform/raspberrypi/linker/kernel.ld +++ /dev/null @@ -1,167 +0,0 @@ -/* - * SPDX-License-Identifier: MIT OR BlueOak-1.0.0 - * Copyright (c) 2018 Andre Richter - * Copyright (c) Berkus Decker - * Original code distributed under MIT, additional changes are under BlueOak-1.0.0 - */ - -__PAGE_SIZE = 64K; -__PAGE_MASK = __PAGE_SIZE - 1; - -__PHYS_MEM_START = 0x0; -__KERNEL_VIRT_ADDR_BASE = 0xffff000000000000; /* Nucleus is mapped here, init_thread is identity-mapped */ - -__PHYS_LOAD_ADDR = 0x80000; -ENTRY(__PHYS_LOAD_ADDR); - -/* Flags: - * 4 == R - * 5 == RX - * 6 == RW - * - * Segments are marked PT_LOAD below so that the ELF file provides virtual and physical addresses. - * It doesn't mean all of them need actually be loaded. - */ -PHDRS -{ - segment_boot_core_stack PT_LOAD FLAGS(6); - segment_code PT_LOAD FLAGS(5); - segment_data PT_LOAD FLAGS(6); -} - -/* Symbols between __INIT_THREAD_START and __INIT_THREAD_END should be dropped after init is complete. - Symbols between __CODE_START and __CODE_END are the kernel code. - Symbols between __BSS_START and __BSS_END must be initialized to zero by startup code in the kernel. - TODO: have two BSSs? init_thread.bss and kernel-high.bss -*/ -SECTIONS -{ - . = __PHYS_MEM_START; - __INIT_THREAD_UNTYPED_START = .; - - /*********************************************************************************************** - * Boot Core Stack - ***********************************************************************************************/ - .boot_core_stack (NOLOAD) : - { - __STACK_BOTTOM = .; /* ^ */ - /* | stack */ - . = __PHYS_LOAD_ADDR; /* | growth AArch64 boot address is 0x80000, 4K-aligned */ - /* | direction */ - __STACK_TOP = .; /* | Stack grows from here towards 0x0. */ - } :segment_boot_core_stack - - ASSERT((__STACK_TOP & __PAGE_MASK) == 0, "End of boot core stack is not page aligned") - - /*********************************************************************************************** - * Code + RO Data - ***********************************************************************************************/ - - .text : ALIGN(__PAGE_SIZE) - { - /******************************************************************************************* - * Boot Code + Boot Data - *******************************************************************************************/ - - KEEP(*(.text.main.entry)) - *(.init_thread.text) - *(.init_thread.data) - *(.init_thread.bss) - *(.text.boot) - *(.data.boot) - - . = ALIGN(__PAGE_SIZE); - - __INIT_THREAD_UNTYPED_END = .; /* Here the boot code ends */ - ASSERT((__INIT_THREAD_UNTYPED_END & __PAGE_MASK) == 0, "End of boot code is not page aligned") - } - - /* Physical addresses */ - __nucleus_start = .; - __CODE_START = .; - __kernel_virt_addr_space_start = .; - - /* Kernel virtual address */ - . = __KERNEL_VIRT_ADDR_BASE; - - .text : AT(__nucleus_start) - { - /******************************************************************************************* - * Regular Kernel Code - *******************************************************************************************/ - - __CODE_START = .; - - *(.text*) - - . = ALIGN(2048); - } :segment_code - - .vectors : ALIGN(2048) - { - __EXCEPTION_VECTORS_START = .; - - KEEP(*(.vectors)) - - . = ALIGN(4); - } :segment_code - - .rodata : ALIGN(4) - { - *(.rodata*) - FILL(0x00) - - . = ALIGN(__PAGE_SIZE); /* Fill up to page size */ - - __CODE_END = .; - ASSERT((__CODE_END & __PAGE_MASK) == 0, "End of kernel code is not page aligned") - } :segment_code - - /*********************************************************************************************** - * Data + BSS - ***********************************************************************************************/ - - .data : ALIGN(__PAGE_SIZE) - { - __DATA_START = .; - ASSERT((__DATA_START & __PAGE_MASK) == 0, "Start of kernel data is not page aligned") - - *(.data*) - FILL(0x00) - - } :segment_data - - .bss (NOLOAD) : ALIGN(__PAGE_SIZE) - { - __BSS_START = LOADADDR(.bss); /* LMA (PhysAddr) */ - *(.bss*) - . = ALIGN(__PAGE_SIZE); /* Align up to page size */ - } :segment_data - - __BSS_END = __BSS_START + ALIGN(SIZEOF(.bss), __PAGE_SIZE); /* LMA, Physical address */ - __DATA_END = .; /* VMA, Virtual address */ - - /*********************************************************************************************** - * MMIO Remap Reserved (8Mb) - ***********************************************************************************************/ - __MMIO_REMAP_START = .; /* FIXME: VMA? Must be LMA perhaps, the point is to remap this */ - . += 8 * 1024 * 1024; - __MMIO_REMAP_END = .; /* FIXME: VMA? Must be LMA perhaps, the point is to remap this */ - - ASSERT((. & __PAGE_MASK) == 0, "MMIO remap reservation is not page aligned") - __kernel_virt_addr_space_size = . - __kernel_virt_addr_space_start; - - /* Physical end */ - __nucleus_end = __nucleus_start + __kernel_virt_addr_space_size; - - /*********************************************************************************************** - * Misc - ***********************************************************************************************/ - - .got : { *(.got*) } - ASSERT(SIZEOF(.got) == 0, "Unexpected relocations in kernel binary.") - - /DISCARD/ : { *(.comment*) *(.gnu*) *(.note*) *(.eh_frame*) *(.text.chainboot*) } -} - -INCLUDE libs/exception/src/arch/aarch64/linker/aarch64-exceptions.ld diff --git a/libs/platform/src/platform/raspberrypi/linker/nucleus-new.ld b/libs/platform/src/platform/raspberrypi/linker/nucleus-new.ld deleted file mode 100644 index d3965bb97..000000000 --- a/libs/platform/src/platform/raspberrypi/linker/nucleus-new.ld +++ /dev/null @@ -1,53 +0,0 @@ -/* Kernel linker script: - * Link all init thread code first, everything that runs once and will be dropped. - * Then put the kernel proper code and static data, aligned to the page size. - * The init thread will map it to higher half. - */ - -SECTIONS { - . = 0; - __init_start = .; - - /* Init stack - also reclaimable */ - __init_stack_bottom = .; - .init.stack (NOLOAD) : { - . += 64K; - } - __init_stack_top = .; - - /* Init-only sections - CAN be reclaimed */ - .init.text : { - (.init_thread.text.entry) - (*.init_thread.text) - /* All Phase 0-5 code goes here */ - } - - .init.data : { - (*.init_thread.data) - *(.data.page_tables) /* Initial page tables */ - } - (*.init_thread.bss) - - . = ALIGN(4K); - __init_end = .; - - . = KERNEL_VIRT_BASE; - - /* Regular kernel text - not reclaimable */ - .text : { - *(.text.boot) /* Very first code */ - *(.text .text.*) - } - - .rodata : { *(.rodata .rodata.*) } - .data : { *(.data .data.*) } - - /* BSS */ - .bss (NOLOAD) : { - __bss_start = .; - *(.bss .bss.*) - __bss_end = .; - } - - __kernel_end = .; -} diff --git a/libs/platform/src/platform/raspberrypi/linker/nucleus.ld b/libs/platform/src/platform/raspberrypi/linker/nucleus.ld new file mode 100644 index 000000000..78fdad87b --- /dev/null +++ b/libs/platform/src/platform/raspberrypi/linker/nucleus.ld @@ -0,0 +1,95 @@ +/* + * SPDX-License-Identifier: BlueOak-1.0.0 + * Copyright (c) Berkus Decker + */ + +__PAGE_SIZE = 64K; +__PAGE_MASK = __PAGE_SIZE - 1; + +__KERNEL_VIRT_ADDR_BASE = 0xffff800000000000; /* Nucleus is higher-mem mapped here */ + +/* Flags: + * 4 == R + * 5 == RX + * 6 == RW + * + * Segments are marked PT_LOAD below so that the ELF file provides virtual and physical addresses. + * It doesn't mean all of them need actually be loaded. + */ +PHDRS +{ + segment_code PT_LOAD FLAGS(5); + segment_data PT_LOAD FLAGS(6); +} + +SECTIONS +{ + . = __KERNEL_VIRT_ADDR_BASE; + + .text : ALIGN(__PAGE_SIZE) + { + /******************************************************************************************* + * Regular Kernel Code + *******************************************************************************************/ + *(.text*) + . = ALIGN(2K); + KEEP(*(.vectors)) + . = ALIGN(2K); + } :segment_code + + /* TODO: rodata is currently executable, but doesn't have to... also each individual section must be aligned to 4K at least for mappings to work */ + .rodata : ALIGN(__PAGE_SIZE) + { + *(.rodata*) + FILL(0x00) + . = ALIGN(__PAGE_SIZE); /* Fill up to page size */ + } :segment_code + + /*********************************************************************************************** + * Data + BSS + ***********************************************************************************************/ + + .data : ALIGN(__PAGE_SIZE) + { + *(.data*) + FILL(0x00) + + . = ALIGN(__PAGE_SIZE); + } :segment_data + + .bss (NOLOAD) : ALIGN(__PAGE_SIZE) + { + *(.bss*) + *(COMMON) + . = ALIGN(__PAGE_SIZE); /* Align up to page size */ + } :segment_data + + . += __PAGE_SIZE; /* add guard page between BSS and the stack! */ + + /* Kernel stack virt addr will be mapped beginning from here */ + __STACK_VIRT_BOTTOM = .; + + /*********************************************************************************************** + * MMIO Remap Reserved (8Mb) - TODO: we do not use, instead map it via bump allocator + ***********************************************************************************************/ + /* __MMIO_REMAP_START = .; FIXME: VMA? Must be LMA perhaps, the point is to remap this + . += 8 * 1024 * 1024; + __MMIO_REMAP_END = .; FIXME: VMA? Must be LMA perhaps, the point is to remap this */ + + /*********************************************************************************************** + * Misc + ***********************************************************************************************/ + + .got : { *(.got*) } + ASSERT(SIZEOF(.got) == 0, "Unexpected relocations in kernel binary.") + + /DISCARD/ : { + *(.comment*) + *(.gnu*) + *(.note*) + *(.eh_frame*) + *(.text.chainboot*) + } +} + +INCLUDE libs/exception/src/arch/aarch64/linker/aarch64-exceptions.ld From 606f1d56e315774c1f83cf8201722b615442e0e3 Mon Sep 17 00:00:00 2001 From: Berkus Decker Date: Fri, 6 Feb 2026 12:35:22 +0200 Subject: [PATCH 064/107] wip: clean up boot code --- kernel/init_thread/src/boot.rs | 100 ---------------- kernel/init_thread/src/el_switch.rs | 10 +- kernel/init_thread/src/main.rs | 27 +++-- kernel/nucleus/_build.rs | 10 -- kernel/nucleus/build.rs | 10 ++ libs/boot/src/arch/aarch64/boot.rs | 107 ++++++------------ libs/boot/src/arch/aarch64/boot.s | 12 +- .../raspberrypi/linker/init_thread.ld | 43 ++++--- 8 files changed, 92 insertions(+), 227 deletions(-) delete mode 100644 kernel/init_thread/src/boot.rs delete mode 100644 kernel/nucleus/_build.rs create mode 100644 kernel/nucleus/build.rs diff --git a/kernel/init_thread/src/boot.rs b/kernel/init_thread/src/boot.rs deleted file mode 100644 index 9b98c69e5..000000000 --- a/kernel/init_thread/src/boot.rs +++ /dev/null @@ -1,100 +0,0 @@ -// init_thread/src/boot.rs - Entry point in EL2 - -use core::arch::global_asm; - -global_asm!( - r#" -.section .text.boot -.global _start - -_start: - // Running in EL2 (assuming bootloader drops us here) - // x0 = DTB pointer (from bootloader) - - // Save DTB pointer - mov x19, x0 - - // Check we're in EL2 - mrs x0, CurrentEL - lsr x0, x0, #2 - cmp x0, #2 - b.ne .hang // Not EL2, hang - - // Set up EL2 stack - adrp x0, __stack_top - add x0, x0, :lo12:__stack_top - mov sp, x0 - - // Clear BSS for init_thread - adrp x0, __bss_start - add x0, x0, :lo12:__bss_start - adrp x1, __bss_end - add x1, x1, :lo12:__bss_end -.clear_bss: - cmp x0, x1 - b.ge .bss_done - stp xzr, xzr, [x0], #16 - b .clear_bss -.bss_done: - - // Call Rust init code with DTB pointer - mov x0, x19 - bl init_main - - // Should not return, but if it does... -.hang: - wfe - b .hang -"# -); - -// Bootloader drops us here with: -// x0 = DTB physical address -// x1 = kernel load address (optional, depends on bootloader) -// MMU off, running at EL1 (or EL2 if hypervisor mode) - -// #[unsafe(naked)] -// #[unsafe(no_mangle)] -// #[unsafe(link_section = ".init_thread.text.entry")] -// unsafe extern "C" fn _start() -> ! { -// core::arch::naked_asm!( -// // Disable interrupts -// "msr daifset, #0xf", -// // Get current EL -// "mrs x2, CurrentEL", -// "lsr x2, x2, #2", -// "cmp x2, #2", -// "b.eq from_el2", -// "b setup_el1", -// "from_el2:", -// // Drop from EL2 to EL1 if needed -// "mov x2, #0x3c5", // EL1h, IRQ/FIQ/Abort masked -// "msr spsr_el2, x2", -// "adr x2, setup_el1", -// "msr elr_el2, x2", -// "eret", -// "setup_el1:", -// // Save DTB pointer before we trash registers -// "mov x20, x0", // x20 = DTB phys addr (callee-saved) -// // Set up init stack (in .bss, identity mapped initially) -// "adrp x2, __init_stack_top", -// "add x2, x2, :lo12:__init_stack_top", -// "mov sp, x2", -// // Clear BSS -// "adrp x2, __bss_start", -// "add x2, x2, :lo12:__bss_start", -// "adrp x3, __bss_end", -// "add x3, x3, :lo12:__bss_end", -// "1:", -// "cmp x2, x3", -// "b.ge 2f", -// "str xzr, [x2], #8", -// "b 1b", -// "2:", -// // Call Rust init with DTB pointer -// "mov x0, x20", -// "bl kernel_init_main", -// // Should not return -// "b .", -// ); -// } diff --git a/kernel/init_thread/src/el_switch.rs b/kernel/init_thread/src/el_switch.rs index 29ecb3c07..abbc1d1dd 100644 --- a/kernel/init_thread/src/el_switch.rs +++ b/kernel/init_thread/src/el_switch.rs @@ -118,7 +118,11 @@ pub unsafe fn enable_mmu_and_drop_to_el1( + SCTLR_EL1::M::Enable, ); - // SPSR_EL2: EL1h with DAIF masked + // Set Saved Program Status Register (EL2) + // Set up a simulated exception return. + // + // Fake a saved program status, where all interrupts were + // masked and SP_EL1 was used as a stack pointer. SPSR_EL2.write( SPSR_EL2::D::Masked + SPSR_EL2::A::Masked @@ -127,7 +131,7 @@ pub unsafe fn enable_mmu_and_drop_to_el1( + SPSR_EL2::M::EL1h, // Use SP_EL1, Return to EL1 ); - // TODO: Mark interrupts in EL1 + // TODO: Mask interrupts in EL1 SPSR_EL1.write( SPSR_EL1::D::Masked + SPSR_EL1::A::Masked @@ -145,7 +149,7 @@ pub unsafe fn enable_mmu_and_drop_to_el1( // ═══════════════════════════════════════════════════════════ // STEP 5: Drop to EL1 // ═══════════════════════════════════════════════════════════ - asm::eret() + asm::eret() // FIXME this doesn't pass DTB (and we hopefully don't need it anymore) } /// Invalidate all TLB entries diff --git a/kernel/init_thread/src/main.rs b/kernel/init_thread/src/main.rs index 14776b500..827d458d9 100644 --- a/kernel/init_thread/src/main.rs +++ b/kernel/init_thread/src/main.rs @@ -34,7 +34,6 @@ // - threads (first version - threads are in-process, kernel has no idea) // - scheduler (invokes process upcall key) -mod boot; mod boot_info; mod device_tree; mod el_switch; @@ -46,13 +45,14 @@ mod paging; use { crate::boot_info::{BOOT_INFO, BootInfoMemRegion}, aarch64_cpu::registers::{SPSR_EL2, Writeable}, - core::{panic::PanicInfo, ptr::write_bytes, slice}, + core::{cell::UnsafeCell, panic::PanicInfo, ptr::write_bytes, slice}, device_tree::{DeviceTree, DeviceTreeProp}, fdt_rs::{ base::DevTree, error::DevTreeError, prelude::{FallibleIterator, PropReader}, }, + libboot::entry, libcpu::endless_sleep, liblocking::interface::Mutex, libmemory::{phys_addr::PhysAddr, virt_addr::VirtAddr}, @@ -63,9 +63,9 @@ use { }; unsafe extern "C" { - static __init_start: u8; - static __init_end: u8; - static __free_memory_start: u8; + static __INIT_START: UnsafeCell<()>; + static __INIT_END: UnsafeCell<()>; + static __FREE_MEMORY_START: UnsafeCell<()>; } #[panic_handler] @@ -81,8 +81,11 @@ fn dump_memory_map() { // TODO print bi.regions instead } -#[unsafe(no_mangle)] -pub extern "C" fn init_main(dtb_ptr: *const u8) -> ! { +entry!(init_main); + +pub fn init_main(dtb: u32) -> ! { + let dtb_ptr = dtb as *const u8; + SPSR_EL2.write( SPSR_EL2::D::Masked + SPSR_EL2::A::Masked @@ -110,9 +113,9 @@ pub extern "C" fn init_main(dtb_ptr: *const u8) -> ! { // Start bump allocator // ───────────────────────────────────────────────────────────────────── - let init_start = unsafe { &__init_start as *const u8 as u64 }; - let init_end = unsafe { &__init_end as *const u8 as u64 }; - let free_start = unsafe { &__free_memory_start as *const u8 as u64 }; + let init_start = unsafe { __INIT_START.get() as u64 }; + let init_end = unsafe { __INIT_END.get() as u64 }; + let free_start = unsafe { __FREE_MEMORY_START.get() as u64 }; let memory_size = 256 * 1024 * 1024; let mut allocator = BootAllocator::new(PhysAddr::new(free_start), memory_size); @@ -358,8 +361,8 @@ pub extern "C" fn init_main(dtb_ptr: *const u8) -> ! { } } -#[unsafe(no_mangle)] -pub extern "C" fn init_thread_run(_dtb_ptr: *const u8) -> ! { +// DTB should be available to this code through BOOT_INFO records. +pub fn init_thread_run() -> ! { // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ // PHASE 5: Initialize kernel objects and structures // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ diff --git a/kernel/nucleus/_build.rs b/kernel/nucleus/_build.rs deleted file mode 100644 index 9c07082d1..000000000 --- a/kernel/nucleus/_build.rs +++ /dev/null @@ -1,10 +0,0 @@ -//! This build script is used to link main kernel binary. - -// const LINKER_SCRIPT: &str = "libs/platform/src/platform/raspberrypi/linker/kernel.ld"; -// const LINKER_SCRIPT_AUX: &str = "libs/exception/src/arch/aarch64/linker/aarch64-exceptions.ld"; - -// fn main() { -// println!("cargo:rerun-if-env-changed=TARGET_BOARD"); -// println!("cargo:rerun-if-changed={LINKER_SCRIPT}"); -// println!("cargo:rerun-if-changed={LINKER_SCRIPT_AUX}"); -// } diff --git a/kernel/nucleus/build.rs b/kernel/nucleus/build.rs new file mode 100644 index 000000000..17f721b29 --- /dev/null +++ b/kernel/nucleus/build.rs @@ -0,0 +1,10 @@ +//! This build script is used to link main kernel binary. + +const LINKER_SCRIPT: &str = "libs/platform/src/platform/raspberrypi/linker/nucleus.ld"; +const LINKER_SCRIPT_AUX: &str = "libs/exception/src/arch/aarch64/linker/aarch64-exceptions.ld"; + +fn main() { + println!("cargo:rerun-if-env-changed=TARGET_BOARD"); + println!("cargo:rerun-if-changed={LINKER_SCRIPT}"); + println!("cargo:rerun-if-changed={LINKER_SCRIPT_AUX}"); +} diff --git a/libs/boot/src/arch/aarch64/boot.rs b/libs/boot/src/arch/aarch64/boot.rs index 743b564f8..10de2a2ed 100644 --- a/libs/boot/src/arch/aarch64/boot.rs +++ b/libs/boot/src/arch/aarch64/boot.rs @@ -62,28 +62,12 @@ pub unsafe extern "C" fn _startup_in_rust(dtb: u32) -> ! { // On entry, w0 should contain the dtb address. // For non-primary cores it contains 0. - // Can't match values with dots in match, so use intermediate consts. + // Can't match values with dots in names in match, so use intermediate consts. #[cfg(feature = "qemu")] const EL3: u64 = CurrentEL::EL::EL3.value; const EL2: u64 = CurrentEL::EL::EL2.value; - const EL1: u64 = CurrentEL::EL::EL1.value; - shared_setup_and_enter_pre(); - - match CurrentEL.get() { - #[cfg(feature = "qemu")] - EL3 => setup_and_enter_el2_from_el3(dtb), - EL2 => setup_and_enter_el2_from_el2(dtb), - EL1 => reset(dtb), // Cannot configure memory mappings here properly, fail instead! - // if not core0 or not EL3/EL2, infinitely wait for events - _ => endless_sleep(), - } -} - -#[unsafe(link_section = ".text.boot")] -#[inline(always)] -fn shared_setup_and_enter_pre() { - // Enable timer counter registers for EL1 + // Enable timer counter registers for EL1 - FIXME can be done later after reset CNTHCTL_EL2.write(CNTHCTL_EL2::EL1PCEN::SET + CNTHCTL_EL2::EL1PCTEN::SET); // No virtual offset for reading the counters @@ -103,67 +87,27 @@ fn shared_setup_and_enter_pre() { ); // Same for SCTLR_EL2? - - // enable_armv6_unaligned_access(); + SCTLR_EL2.write( + SCTLR_EL2::I::NonCacheable + + SCTLR_EL2::C::NonCacheable + + SCTLR_EL2::M::Disable + + SCTLR_EL2::A::Disable + + SCTLR_EL2::SA::Disable, + ); // Set Hypervisor Configuration Register (EL2) // Set EL1 execution state to AArch64 // @todo Explain the SWIO bit (SWIO hardwired on Pi3) HCR_EL2.write(HCR_EL2::RW::EL1IsAarch64 + HCR_EL2::SWIO::SET); // @todo disable VM bit to prevent stage 2 MMU translations -} -// #[unsafe(link_section = ".text.boot")] -// #[inline] -// fn shared_setup_and_enter_post(dtb: u32) -> ! { -// unsafe extern "Rust" { -// // Stack top -// static __STACK_TOP: UnsafeCell<()>; -// } -// // Set up SP_EL2 (stack pointer), which will be used by EL2 once -// // we "return" to it. -// // SAFETY: Pure asm. -// unsafe { -// SP_EL2.set(__STACK_TOP.get() as u64); -// } -// // Use `eret` to "return" to EL2. This will result in execution of -// // `reset()` in EL2. -// // Load DTB address into w0 prior to eret. -// unsafe { -// core::arch::asm!("eret", in("w0") dtb); -// core::hint::unreachable_unchecked() -// } -// } - -// FIXME: This will be called by init_thread later. -/// Real hardware boot-up sequence. -/// -/// Prepare and execute transition from EL2 to EL1. -// #[unsafe(link_section = ".text.boot")] -// #[inline] -// fn setup_and_enter_el1_from_el2(dtb: u32) -> ! { -// // Set Saved Program Status Register (EL2) -// // Set up a simulated exception return. -// // -// // Fake a saved program status, where all interrupts were -// // masked and SP_EL1 was used as a stack pointer. -// SPSR_EL2.write( -// SPSR_EL2::D::Masked -// + SPSR_EL2::A::Masked -// + SPSR_EL2::I::Masked -// + SPSR_EL2::F::Masked -// + SPSR_EL2::M::EL1h, // Use SP_EL1 -// ); -// // Make the Exception Link Register (EL2) point to reset(). -// #[allow(clippy::fn_to_numeric_cast_any)] -// ELR_EL2.set(reset as *const () as u64); -// shared_setup_and_enter_post(dtb) -// } - -#[unsafe(link_section = ".text.boot")] -#[inline] -fn setup_and_enter_el2_from_el2(dtb: u32) -> ! { - reset(dtb); + match CurrentEL.get() { + #[cfg(feature = "qemu")] + EL3 => setup_and_enter_el2_from_el3(dtb), + EL2 => reset(dtb), + // Cannot configure memory mappings in all other cases, fail instead! + _ => endless_sleep(), + } } /// QEMU boot-up sequence. @@ -173,6 +117,7 @@ fn setup_and_enter_el2_from_el2(dtb: u32) -> ! { /// section: 5.5.1 /// However, GPU init code must be switching it down to EL2. /// QEMU can't emulate Raspberry Pi properly (no VC boot code), so it starts in EL3. +/// FIXME: new qemu has seemingly fixed this, so probably not needed anymore, double check! /// /// Prepare and execute transition from EL3 to EL2. /// (from https://github.com/s-matyukevich/raspberry-pi-os/blob/master/docs/lesson02/rpi-os.md) @@ -199,7 +144,23 @@ fn setup_and_enter_el2_from_el3(dtb: u32) -> ! { // Make the Exception Link Register (EL3) point to reset(). ELR_EL3.set(reset as *const () as u64); - shared_setup_and_enter_post(dtb) + unsafe extern "Rust" { + // Stack top + static __STACK_TOP: UnsafeCell<()>; + } + // Set up SP_EL2 (stack pointer), which will be used by EL2 once + // we "return" to it. + // SAFETY: Pure asm. + unsafe { + SP_EL2.set(__STACK_TOP.get() as u64); + } + // Use `eret` to "return" to EL2. This will result in execution of + // `reset()` in EL2. + // Load DTB address into w0 prior to eret. + unsafe { + core::arch::asm!("eret", in("w0") dtb); + core::hint::unreachable_unchecked() + } } // Enter Rust code in EL2. diff --git a/libs/boot/src/arch/aarch64/boot.s b/libs/boot/src/arch/aarch64/boot.s index e8cb4f8ec..b6ea79404 100644 --- a/libs/boot/src/arch/aarch64/boot.s +++ b/libs/boot/src/arch/aarch64/boot.s @@ -1,14 +1,11 @@ -// SPDX-License-Identifier: MIT OR Apache-2.0 -// -// Copyright (c) 2021 Andre Richter -// Modifications -// Copyright (c) 2021- Berkus +// SPDX-License-Identifier: BlueOak-1.0.0 +// Copyright (c) Berkus Decker // // Pre-boot code. // Used only because Rust's abstract machine considers UB any access to statics // before statics have been initialized. This is exactly the case for the boot code. -// So we avoid referencing any statics in the Rust code, and delegate the +// So we avoid referencing any linker symbol statics from the Rust code, and delegate the // task to assembly piece instead. // @@ -48,7 +45,7 @@ /// Entrypoint of the processor. /// /// Parks all cores except core0 and checks if we started in EL2/EL3. If -/// so, proceeds with setting up EL1. +/// so, init BSS and enter Rust code. /// /// This is invoked from the linker script, does arch-specific init /// and passes control to the kernel main function in Rust. @@ -83,6 +80,7 @@ _boot_cores: ADR_ABS x1, __STACK_TOP mov sp, x1 + // On entry, x0 contains DTB address bl _startup_in_rust .L_parking_loop: diff --git a/libs/platform/src/platform/raspberrypi/linker/init_thread.ld b/libs/platform/src/platform/raspberrypi/linker/init_thread.ld index 3abdf39fd..ddafec6ff 100644 --- a/libs/platform/src/platform/raspberrypi/linker/init_thread.ld +++ b/libs/platform/src/platform/raspberrypi/linker/init_thread.ld @@ -1,57 +1,56 @@ -/* init_thread/init.ld - Identity-mapped init thread */ +/* + * Identity-mapped init thread. + * SPDX-License-Identifier: BlueOak-1.0.0 + * Copyright (c) Berkus Decker + */ -ENTRY(_start) +ENTRY(_boot_cores) /* Physical load address - typical for RPi4 */ -INIT_BASE = 0x80000; +__INIT_BASE = 0x80000; SECTIONS { /* Stack for init thread (grows down) */ - . = 0; - __stack_bottom = .; - . = INIT_BASE; - __stack_top = .; + . = 4K; + __STACK_BOTTOM = .; + . = __INIT_BASE; + __STACK_TOP = .; - __init_start = .; + __INIT_START = .; - /* Entry point and early code */ - .text.boot ALIGN(4K) : - { - *(.text.boot) - } - - .text ALIGN(4K) : + .text : ALIGN(4K) { + *(.text.main.entry) /* Entry point and early code */ *(.text .text.*) } - .rodata ALIGN(4K) : + .rodata : ALIGN(4K) { *(.rodata .rodata.*) /* Embedded kernel will be here via include_bytes! */ } - .data ALIGN(4K) : + .data : ALIGN(4K) { *(.data .data.*) } - .bss ALIGN(4K) : + .bss : ALIGN(4K) { - __bss_start = .; + __BSS_START = .; *(.bss .bss.*) *(COMMON) - __bss_end = .; + __BSS_END = .; } . = ALIGN(4K); - __init_end = .; + __INIT_END = .; /* Free memory starts here - used for kernel placement */ . = ALIGN(2M); /* 2MB aligned for huge pages if desired */ - __free_memory_start = .; + __FREE_MEMORY_START = .; /DISCARD/ : { From 2a6ab6c904a2e67316781f2d6e3195fbaadab977 Mon Sep 17 00:00:00 2001 From: Berkus Decker Date: Fri, 6 Feb 2026 14:01:02 +0200 Subject: [PATCH 065/107] wip: clean up old init thread code, split to userspace --- kernel/Plan.md | 4 +- kernel/init_thread/src/bits.rs | 219 --------------------------------- kernel/init_thread/src/main.rs | 88 ++++++++++++- userspace/shell.rs | 4 +- 4 files changed, 90 insertions(+), 225 deletions(-) delete mode 100644 kernel/init_thread/src/bits.rs diff --git a/kernel/Plan.md b/kernel/Plan.md index 456869571..0ca7f0d7f 100644 --- a/kernel/Plan.md +++ b/kernel/Plan.md @@ -19,12 +19,14 @@ Steps: - [x] Fill it with capability to DebugConsole - [x] Invoke DebugConsole.Write via syscall +- [ ] Make some caps work - Untypeds, Domains, Buffers, what else? +- [ ] Test out syscalls from EL0 + - [ ] Print kernel covered area - [ ] Print KERNEL_HIGH_BASE - [ ] Print kernel mappings size and attribs - [ ] Print init_thread covered area - [ ] Print init_thread mappings size -- [ ] Make some caps work - Untypeds, Domains, Buffers, what else? - START FILLING IN CAPS - [ ] untypeds - [ ] init_thread context and domain diff --git a/kernel/init_thread/src/bits.rs b/kernel/init_thread/src/bits.rs deleted file mode 100644 index 00d0314a8..000000000 --- a/kernel/init_thread/src/bits.rs +++ /dev/null @@ -1,219 +0,0 @@ -libboot::entry!(init_thread); - -/// Kernel early init code. -/// `arch` crate is responsible for calling it. -/// -/// # Safety -/// -/// - Only a single core must be active and running this function. -/// - The init calls in this function must appear in the correct order: -/// - MMU + Data caching must be activated at the earliest. Without it, any atomic operations, -/// e.g. the yet-to-be-introduced spinlocks in the device drivers (which currently employ -/// `IRQSafeNullLocks` instead of spinlocks), will fail to work (properly) on the `RPi` `SoCs`. -pub unsafe fn init_thread() -> ! { - // Entered in EL2: - - early_print!("Entered EL2"); - - early_print!("DTB at"); - early_print!("Max RAM size is"); - early_print!("Init thread image covers phys -:- identity mapped"); - early_print!("Init thread mapping tables filled in as - entries"); - early_print!("Kernel image covers phys -:- mapped to KERNEL_HIGH_BASE:-"); - early_print!("Kernel mapping tables filled in as - for kernel, as - for phys memory"); - - // #[cfg(feature = "jtag")] - // libmachine::debug::jtag::wait_debugger(); - - // libexception::exception::handling_init(); - - // SAFETY: Not safe! - let phys_kernel_tables_base_addr = match unsafe { libmemory::mmu::kernel_map_binary() } { - Err(string) => panic!("Error mapping kernel binary: {}", string), - Ok(addr) => addr, - }; - - // SAFETY: Not safe! - if let Err(e) = unsafe { libmemory::mmu::enable_mmu_and_caching(phys_kernel_tables_base_addr) } - { - panic!("Enabling MMU failed: {}", e); - } - - libmemory::mmu::post_enable_init(); - - // After page tables are populated and MMU is on, switch to EL1, now kernel will already be higher-half mapped. - - // SAFETY: Not safe! - // if let Err(x) = unsafe { libplatform::platform::drivers::init() } { - // panic!("Error initializing platform drivers: {}", x); - // } - - // Initialize all device drivers. - // SAFETY: Not safe! - // unsafe { - // libplatform::platform::drivers::driver_manager().init_drivers_and_irqs(); - // } - - // Unmask interrupts on the boot CPU core. - // libexception::exception::asynchronous::local_irq_unmask(); - - // Announce conclusion of the kernel_init() phase. - // libkernel_state::state_manager().transition_to_single_core_main(); - - // libconsole::init_logger(); - - // info!("{}", libkernel::version()); - // info!("Booting on: {}", bsp::board_name()); - - info!( - "{} version {}", - env!("CARGO_PKG_NAME"), - env!("CARGO_PKG_VERSION") - ); - info!( - "Booting on: {}", - libplatform::platform::BcmHost::board_name() - ); - - // info!("MMU online. Special regions:"); - // machine::platform::memory::mmu::virt_mem_layout().print_layout(); - - dump_memory_map(); - - let (_, privilege_level) = libexception::exception::current_privilege_level(); - info!("Current privilege level: {privilege_level}"); - - info!("Exception handling state:"); - libexception::exception::asynchronous::print_state(); - - info!( - "Architectural timer resolution: {} ns", - libtime::time::time_manager().resolution().as_nanos() - ); - - info!("Drivers loaded:"); - libplatform::platform::drivers::driver_manager().enumerate(); - - info!("Registered IRQ handlers:"); - libplatform::platform::exception::asynchronous::irq_manager().print_handler(); - - // Test a failing timer case. - libtime::time::time_manager().spin_for(Duration::from_nanos(1)); - - for _ in 0..3 { - info!("Spinning for 1 second"); - libtime::time::time_manager().spin_for(Duration::from_secs(1)); - } - - command_prompt(); - - reboot() -} - -fn print_mmu_state_and_features() { - libmemory::arch::features::print_features(); -} - -fn dump_memory_map() { - // Output the memory map as we could derive from FDT and information about our loaded image - // Use it to imagine how the memmap would look like in the end. - // arch::memory::print_layout(); -} - -//------------------------------------------------------------ -// Start a command prompt -//------------------------------------------------------------ -fn command_prompt() { - 'cmd_loop: loop { - let mut buf = [0_u8; 64]; - - match libconsole::console::command_prompt(&mut buf) { - // b"mmu" => init_mmu(), - b"feats" => print_mmu_state_and_features(), - // b"disp" => check_display_init(), - // b"trap" => check_data_abort_trap(), - // b"map" => machine::platform::memory::mmu::virt_mem_layout().print_layout(), - // b"led on" => set_led(true), - // b"led off" => set_led(false), - b"help" => print_help(), - b"end" => break 'cmd_loop, - x => warn!("[!] Unknown command {x:?}, try 'help'"), - } - } -} - -fn print_help() { - println!("Supported console commands:"); - println!(" mmu - initialize MMU"); - println!(" feats - print MMU state and supported features"); - #[cfg(not(feature = "noserial"))] - println!(" uart - try to reinitialize UART serial"); - // println!(" disp - try to init VC framebuffer and draw some text"); - println!(" trap - trigger and recover from a data abort exception"); - println!(" map - show kernel memory layout"); - // println!(" led [on|off] - change RPi LED status"); - println!(" end - leave console and reset board"); -} - -// fn set_led(enable: bool) { -// let mut mbox = Mailbox::<8>::default(); -// let index = mbox.request(); -// let index = mbox.set_led_on(index, enable); -// let mbox = mbox.end(index); -// -// mbox.call(channel::PropertyTagsArmToVc) -// .map_err(|e| { -// warn!("Mailbox call returned error {}", e); -// warn!("Mailbox contents: {:?}", mbox); -// }) -// .ok(); -// } - -fn reboot() -> ! { - cfg_if! { - if #[cfg(feature = "qemu")] { - info!("Bye, shutting down QEMU"); - libqemu::semihosting::exit_success() - } else { - // use machine::platform::raspberrypi::power::Power; - - info!("Bye, going to reset now"); - // Power::default().reset() - libcpu::endless_sleep() - } - } -} - -// fn check_display_init() { -// display_graphics() -// .map_err(|e| { -// warn!("Error in display: {}", e); -// }) -// .ok(); -// } -// -// fn display_graphics() -> Result<(), DrawError> { -// if let Ok(mut display) = VC::init_fb(800, 600, 32) { -// info!("Display created"); -// -// display.clear(Color::black()); -// info!("Display cleared"); -// -// display.rect(10, 10, 250, 250, Color::rgb(32, 96, 64)); -// display.draw_text(50, 50, "Hello there!", Color::rgb(128, 192, 255))?; -// -// let mut buf = [0u8; 64]; -// let s = machine::write_to::show(&mut buf, format_args!("Display width {}", display.width)); -// -// if s.is_err() { -// display.draw_text(50, 150, "Error displaying", Color::red())? -// } else { -// display.draw_text(50, 150, s.unwrap(), Color::white())? -// } -// -// display.draw_text(150, 50, "RED", Color::red())?; -// display.draw_text(160, 60, "GREEN", Color::green())?; -// display.draw_text(170, 70, "BLUE", Color::blue())?; -// } -// Ok(()) -// } diff --git a/kernel/init_thread/src/main.rs b/kernel/init_thread/src/main.rs index 827d458d9..ca218ae9f 100644 --- a/kernel/init_thread/src/main.rs +++ b/kernel/init_thread/src/main.rs @@ -81,9 +81,20 @@ fn dump_memory_map() { // TODO print bi.regions instead } -entry!(init_main); - -pub fn init_main(dtb: u32) -> ! { +entry!(init_main_el2); + +/// Kernel early init code. +/// `arch` crate is responsible for calling it. +/// +/// # Safety +/// +/// - Only a single core must be active and running this function. +/// - The init calls in this function must appear in the correct order: +/// - MMU + Data caching must be activated at the earliest. Without it, any atomic operations, +/// e.g. the yet-to-be-introduced spinlocks in the device drivers (which currently employ +/// `IRQSafeNullLocks` instead of spinlocks), will fail to work (properly) on the `RPi` `SoCs`. + +pub fn init_main_el2(dtb: u32) -> ! { let dtb_ptr = dtb as *const u8; SPSR_EL2.write( @@ -94,6 +105,9 @@ pub fn init_main(dtb: u32) -> ! { + SPSR_EL2::M::EL1h, // Use SP_EL1/2 ); + #[cfg(feature = "jtag")] + libmachine::debug::jtag::wait_debugger(); + semi_println!("init_main started"); // unsafe { @@ -350,6 +364,11 @@ pub fn init_main(dtb: u32) -> ! { // PHASE 4: Enable MMU and drop to EL1 // ═══════════════════════════════════════════════════════════════ + semi_println!("Init thread image covers phys -:- identity mapped"); + semi_println!("Init thread mapping tables filled in as - entries"); + semi_println!("Kernel image covers phys -:- mapped to KERNEL_HIGH_BASE:-"); + semi_println!("Kernel mapping tables filled in as - for kernel, as - for phys memory"); + unsafe { el_switch::enable_mmu_and_drop_to_el1( ttbr0, @@ -494,6 +513,12 @@ pub fn init_thread_run() -> ! { let err = DebugConsoleKey::new_slot(KeySlot::CAPTBL_SELF); err.write("DEBCON| Invalid capability invocation - no output"); + let (_, privilege_level) = libexception::current_privilege_level(); + liblog::info!("Current privilege level: {privilege_level}"); + + liblog::info!("Exception handling state:"); + libexception::asynchronous::print_state(); + // semi_println!("Switching to init domain..."); // semi_println!("═══════════════════════════════════════════════════════════"); @@ -506,6 +531,63 @@ pub fn init_thread_run() -> ! { // // This never returns // switch_to_domain(init_domain, init_time); libqemu::semihosting::exit_success() + + // libmemory::mmu::post_enable_init(); // kernel_init_mmio_va_allocator + + // SAFETY: Not safe! + // if let Err(x) = unsafe { libplatform::platform::drivers::init() } { + // panic!("Error initializing platform drivers: {}", x); + // } + + // Initialize all device drivers. + // SAFETY: Not safe! + // unsafe { + // libplatform::platform::drivers::driver_manager().init_drivers_and_irqs(); + // } + + // Unmask interrupts on the boot CPU core. + // libexception::exception::asynchronous::local_irq_unmask(); + + // Announce conclusion of the kernel_init() phase. + // libkernel_state::state_manager().transition_to_single_core_main(); + + // libconsole::init_logger(); + + // info!("{}", libkernel::version()); + + // info!( + // "{} version {}", + // env!("CARGO_PKG_NAME"), + // env!("CARGO_PKG_VERSION") + // ); + // info!( + // "Booting on: {}", + // libplatform::platform::BcmHost::board_name() + // ); + + // info!("MMU online. Special regions:"); + // machine::platform::memory::mmu::virt_mem_layout().print_layout(); + + // dump_memory_map(); + + // info!( + // "Architectural timer resolution: {} ns", + // libtime::time::time_manager().resolution().as_nanos() + // ); + + // info!("Drivers loaded:"); + // libplatform::platform::drivers::driver_manager().enumerate(); + + // info!("Registered IRQ handlers:"); + // libplatform::platform::exception::asynchronous::irq_manager().print_handler(); + + // // Test a failing timer case. + // libtime::time::time_manager().spin_for(Duration::from_nanos(1)); + + // for _ in 0..3 { + // info!("Spinning for 1 second"); + // libtime::time::time_manager().spin_for(Duration::from_secs(1)); + // } } /* // ───────────────────────────────────────────────────────────────────── diff --git a/userspace/shell.rs b/userspace/shell.rs index 6dcb6c647..f9b7c0e0c 100644 --- a/userspace/shell.rs +++ b/userspace/shell.rs @@ -1,9 +1,9 @@ fn print_mmu_state_and_features() { // use machine::memory::mmu::interface::MMU; - // memory::mmu::mmu().print_features(); + libmemory::arch::features::print_features(); } -// TODO: AFTER INIT_THREAD, one of the userspace processes! +// TODO: AFTER INIT_THREAD, one of the userspace processes // //------------------------------------------------------------ // Start a command prompt From a7fd69d9d403baa84cdc7b12e0770def7b5de87c Mon Sep 17 00:00:00 2001 From: Berkus Decker Date: Fri, 6 Feb 2026 14:26:09 +0200 Subject: [PATCH 066/107] wip: split libmemory into libaddress and libpaging (or libmmu?) --- Cargo.toml | 3 + kernel/init_thread/Cargo.toml | 1 + kernel/init_thread/src/boot_info.rs | 8 +- kernel/init_thread/src/loader.rs | 12 +- kernel/init_thread/src/main.rs | 6 +- kernel/init_thread/src/memory.rs | 20 +- kernel/init_thread/src/paging.rs | 26 +- kernel/nucleus/Cargo.toml | 2 +- kernel/nucleus/src/api/debug_console.rs | 2 +- .../src/objects/arch/aarch64_objects.rs | 2 +- kernel/nucleus/src/objects/arch/frame.rs | 2 +- kernel/nucleus/src/objects/arch/page_table.rs | 2 +- kernel/nucleus/src/objects/arch_objects.rs | 2 +- kernel/nucleus/src/objects/debug_console.rs | 2 +- kernel/nucleus/src/objects/domain.rs | 4 +- kernel/tests/memory.rs | 19 +- libs/address/Cargo.toml | 41 ++ libs/address/README.md | 18 + libs/address/src/align.rs | 79 +++ libs/address/src/lib.rs | 613 ++++++++++++++++++ libs/{memory => address}/test/memory_test.rs | 0 libs/boot/Cargo.toml | 2 +- libs/boot/src/arch/aarch64/boot.rs | 8 +- libs/memory/Cargo.toml | 1 + libs/memory/README.md | 19 + libs/memory/src/arch/aarch64/addr/docs.md | 12 + libs/memory/src/arch/aarch64/mmu/mod.rs | 2 +- .../src/arch/aarch64/mmu/translation_table.rs | 15 +- libs/memory/src/arch/aarch64/mod.rs | 2 +- libs/memory/src/arch/aarch64/phys_frame.rs | 6 +- libs/memory/src/arch/aarch64/virt_page.rs | 10 +- libs/memory/src/lib.rs | 150 +---- libs/memory/src/mm/bump_allocator.rs | 11 +- libs/memory/src/mm/mod.rs | 68 -- libs/memory/src/mmu/mapping_record.rs | 28 +- libs/memory/src/mmu/mod.rs | 23 +- libs/memory/src/mmu/page_alloc.rs | 8 +- libs/memory/src/mmu/translation_table.rs | 2 +- libs/memory/src/mmu/types.rs | 61 +- libs/memory/src/phys_addr.rs | 254 -------- .../src/platform/raspberrypi/memory/mmu.rs | 16 +- .../src/platform/raspberrypi/memory/mod.rs | 15 +- libs/memory/src/virt_addr.rs | 273 -------- libs/object/Cargo.toml | 1 + libs/object/src/domain.rs | 4 +- libs/platform/Cargo.toml | 1 + libs/platform/README.md | 7 + .../raspberrypi/device_driver/bcm/gpio.rs | 2 +- .../bcm/interrupt_controller/mod.rs | 2 +- .../bcm/interrupt_controller/peripheral_ic.rs | 2 +- .../raspberrypi/device_driver/bcm/mailbox.rs | 2 +- .../device_driver/bcm/mini_uart.rs | 2 +- .../device_driver/bcm/pl011_uart.rs | 2 +- .../raspberrypi/device_driver/bcm/power.rs | 2 +- .../raspberrypi/device_driver/common.rs | 2 +- libs/platform/src/platform/raspberrypi/fb.rs | 2 +- 56 files changed, 998 insertions(+), 883 deletions(-) create mode 100644 libs/address/Cargo.toml create mode 100644 libs/address/README.md create mode 100644 libs/address/src/align.rs create mode 100644 libs/address/src/lib.rs rename libs/{memory => address}/test/memory_test.rs (100%) create mode 100644 libs/memory/README.md create mode 100644 libs/memory/src/arch/aarch64/addr/docs.md delete mode 100644 libs/memory/src/phys_addr.rs delete mode 100644 libs/memory/src/virt_addr.rs create mode 100644 libs/platform/README.md diff --git a/Cargo.toml b/Cargo.toml index 6369697bc..cf2ed4688 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,6 +5,7 @@ members = [ "bin/chainboot", "bin/chainofcommand", # Libraries + "libs/address", "libs/boot", "libs/console", "libs/cpu", @@ -45,9 +46,11 @@ version = "0.0.1" #======== aarch64-cpu = { version = "11.2" } bit_field = { version = "0.10" } +bitfield-struct = { version = "0.12" } bitflags = { version = "2.10" } buddy-alloc = { version = "0.6.0", git = "https://github.com/metta-systems/buddy-alloc", branch = "feature/allocator-api" } cfg-if = { version = "1.0" } +libaddress = { path = "libs/address" } libboot = { path = "libs/boot" } libconsole = { path = "libs/console" } libcpu = { path = "libs/cpu" } diff --git a/kernel/init_thread/Cargo.toml b/kernel/init_thread/Cargo.toml index 3d568226c..8ee91a723 100644 --- a/kernel/init_thread/Cargo.toml +++ b/kernel/init_thread/Cargo.toml @@ -32,6 +32,7 @@ aarch64-cpu = { workspace = true } bit_field = { workspace = true } bitflags = { workspace = true } cfg-if = { workspace = true } +libaddress = { workspace = true } libboot = { workspace = true } libconsole = { workspace = true } libcpu = { workspace = true } diff --git a/kernel/init_thread/src/boot_info.rs b/kernel/init_thread/src/boot_info.rs index 63b26b3ad..b68f8a910 100644 --- a/kernel/init_thread/src/boot_info.rs +++ b/kernel/init_thread/src/boot_info.rs @@ -3,8 +3,8 @@ //! Define a map of memory regions used during boot allocations. use { core::{cell::LazyCell, fmt}, + libaddress::PhysAddr, liblocking::IRQSafeNullLock, - libmemory::phys_addr::PhysAddr, snafu::Snafu, }; @@ -132,7 +132,7 @@ impl BootInfoMemRegion { } /// Calculate region size. - pub fn size(&self) -> u64 { + pub fn size(&self) -> usize { self.end_exclusive - self.start_inclusive } @@ -394,8 +394,8 @@ impl BootInfo { for (i, reg_iter) in self.regions.iter().enumerate() { // Determine whether placing the region at the start or the end will create a bigger left over region. - let aligned_start = reg_iter.start_inclusive.aligned_up(1usize << size_bits); - let aligned_end = reg_iter.end_exclusive.aligned_down(1usize << size_bits); + let aligned_start = reg_iter.start_inclusive.aligned_up(1u64 << size_bits); + let aligned_end = reg_iter.end_exclusive.aligned_down(1u64 << size_bits); let new_reg = if aligned_start - reg_iter.start_inclusive < reg_iter.end_exclusive - aligned_end { diff --git a/kernel/init_thread/src/loader.rs b/kernel/init_thread/src/loader.rs index 37ee5f65d..f386f9cb6 100644 --- a/kernel/init_thread/src/loader.rs +++ b/kernel/init_thread/src/loader.rs @@ -6,7 +6,7 @@ use { memory::{BootAllocator, KernelLayout, MemoryPermissions}, }, core::ptr, - libmemory::{phys_addr::PhysAddr, virt_addr::VirtAddr}, + libaddress::{PhysAddr, VirtAddr}, libqemu::semi_println, }; @@ -93,7 +93,7 @@ pub fn load_kernel(allocator: &mut BootAllocator) -> Result Result Result section.meta.name, section.data.len(), section.meta.size, - dest_phys.0 + dest_phys.as_u64() ); if !dest_phys.as_u64().is_multiple_of(section.meta.alignment) { @@ -184,7 +184,7 @@ fn zero_bss(bss: &SectionMeta, kernel_phys_base: PhysAddr) -> Result<(), &'stati "> section {}, zero {} bytes at {:#016X}", bss.name, bss.size, - dest_phys.0 + dest_phys.as_u64() ); if !dest_phys.as_u64().is_multiple_of(bss.alignment) { diff --git a/kernel/init_thread/src/main.rs b/kernel/init_thread/src/main.rs index ca218ae9f..d0c495cee 100644 --- a/kernel/init_thread/src/main.rs +++ b/kernel/init_thread/src/main.rs @@ -52,10 +52,10 @@ use { error::DevTreeError, prelude::{FallibleIterator, PropReader}, }, + libaddress::{PhysAddr, VirtAddr}, libboot::entry, libcpu::endless_sleep, liblocking::interface::Mutex, - libmemory::{phys_addr::PhysAddr, virt_addr::VirtAddr}, libobject::{DebugConsoleKey, KeySlot}, libqemu::semi_println, libsyscall::protected_call6, @@ -155,7 +155,7 @@ pub fn init_main_el2(dtb: u32) -> ! { let block = allocator .alloc_aligned(layout.size(), layout.align()) .expect("Couldn't allocate DeviceTree index"); - let raw_slice = unsafe { core::slice::from_raw_parts_mut(block.0 as *mut u8, layout.size()) }; + let raw_slice = unsafe { core::slice::from_raw_parts_mut(block.as_mut_ptr(), layout.size()) }; let device_tree = DeviceTree::new(device_tree, raw_slice).expect("Couldn't initialize indexed DeviceTree"); @@ -340,7 +340,7 @@ pub fn init_main_el2(dtb: u32) -> ! { &mut mmu_setup, &kernel_layout, total_memory, - el1_stack.0, + el1_stack.as_u64(), el1_stack_size, ) .expect("Failed to create kernel mapping"); diff --git a/kernel/init_thread/src/memory.rs b/kernel/init_thread/src/memory.rs index 45c77fd63..c4f6ea9a1 100644 --- a/kernel/init_thread/src/memory.rs +++ b/kernel/init_thread/src/memory.rs @@ -2,7 +2,7 @@ use { crate::loader::LoadableSection, - libmemory::{phys_addr::PhysAddr, virt_addr::VirtAddr}, + libaddress::{PhysAddr, VirtAddr}, }; /// Memory region translation. @@ -24,7 +24,7 @@ impl BootAllocator { pub fn new(start: PhysAddr, size: usize) -> Self { Self { current: start, - end: PhysAddr::new(start.0 + size as u64), + end: PhysAddr::new(start.as_u64() + size as u64), } } @@ -33,14 +33,14 @@ impl BootAllocator { } pub fn alloc_aligned(&mut self, size: usize, align: usize) -> Option { - let aligned = self.current.aligned_up(align); - let new_current = PhysAddr::new(aligned.0 + size as u64); + let aligned = self.current.aligned_up(align as u64); + let new_current = PhysAddr::new(aligned.as_u64() + size as u64); libqemu::semi_println!( "alloc_aligned {:#016x} => {:#016x} (wrt {:#016x})", - aligned.0, - new_current.0, - self.end.0 + aligned.as_u64(), + new_current.as_u64(), + self.end.as_u64() ); if new_current > self.end { @@ -57,7 +57,7 @@ impl BootAllocator { self.end } pub fn remaining(&self) -> usize { - (self.end.0 - self.current.0) as usize + self.end - self.current } } @@ -144,9 +144,9 @@ impl KernelLayout { assert!( self.vectors_virt.as_u64() & 0x7FF == 0, "VBAR_EL1 address 0x{:016X} must be 2KB aligned", - self.vectors_virt.0 + self.vectors_virt.as_u64() ); - self.vectors_virt.0 + self.vectors_virt.as_u64() } pub fn iter_sections(&self) -> impl Iterator + '_ { diff --git a/kernel/init_thread/src/paging.rs b/kernel/init_thread/src/paging.rs index 2d7759f0a..1fa60fcaa 100644 --- a/kernel/init_thread/src/paging.rs +++ b/kernel/init_thread/src/paging.rs @@ -19,7 +19,7 @@ use { crate::memory::{BootAllocator, KernelLayout, MemoryPermissions, SectionMapping}, core::ptr, - libmemory::{phys_addr::PhysAddr, virt_addr::VirtAddr}, + libaddress::{PhysAddr, VirtAddr}, libqemu::semi_println, }; @@ -146,9 +146,9 @@ impl<'a> MmuSetup<'a> { l3_table.entries[l3_idx] = phys.as_u64() | flags::VALID | flags::PAGE | pte_flags; semi_println!( - "Mapped 4K page {:#016X} frame {:#016X} in {} with {}", - virt.0, - phys.0, + "Mapped 4K page {} frame {} in {} with {}", + virt, + phys, match ttbr { Ttbr::Ttbr0 => "TTBR0(user)", Ttbr::Ttbr1 => "TTBR1(kernel)", @@ -190,9 +190,9 @@ impl<'a> MmuSetup<'a> { l2_table.entries[l2_idx] = phys.as_u64() | flags::VALID | flags::BLOCK | pte_flags; semi_println!( - "Mapped 2M page {:#016X} frame {:#016X} in {} with {}", - virt.0, - phys.0, + "Mapped 2M page {} frame {} in {} with {}", + virt, + phys, match ttbr { Ttbr::Ttbr0 => "TTBR0(user)", Ttbr::Ttbr1 => "TTBR1(kernel)", @@ -243,8 +243,8 @@ pub fn create_identity_mapping( start: PhysAddr, end: PhysAddr, ) -> Result<(), &'static str> { - let start_aligned = start.aligned_down(2 * 1024 * 1024); - let end_aligned = end.aligned_up(2 * 1024 * 1024); + let start_aligned = start.aligned_down(2u64 * 1024 * 1024); + let end_aligned = end.aligned_up(2u64 * 1024 * 1024); let perms = MemoryPermissions { readable: true, @@ -295,7 +295,7 @@ pub fn create_kernel_mapping( for i in 0..max_ram_bytes.div_ceil(2 * 1024 * 1024) { setup.map_block_2mb( Ttbr::Ttbr1, - VirtAddr::new(libmemory::PHYSICAL_KERNEL_WINDOW + i * 2 * 1024 * 1024), + VirtAddr::new(libaddress::PHYSICAL_KERNEL_WINDOW + i * 2 * 1024 * 1024), PhysAddr::new(i * 2 * 1024 * 1024), perms, ); @@ -307,7 +307,7 @@ pub fn create_kernel_mapping( for i in 0..el1_stack_size.div_ceil(4 * 1024) as u64 { setup.map_page( Ttbr::Ttbr1, - VirtAddr::new(stack_bottom.0 + i * 4 * 1024), + VirtAddr::new(stack_bottom.as_u64() + i * 4 * 1024), PhysAddr::new(el1_stack + i * 4 * 1024), perms, ); @@ -328,13 +328,13 @@ pub fn create_kernel_mapping( /// Map a single section with proper permissions fn map_section(setup: &mut MmuSetup, section: &SectionMapping) -> Result<(), &'static str> { - if !section.phys_start.is_aligned(4096) { + if !section.phys_start.is_aligned(4096u64) { semi_println!("!! Section {} not aligned to 4K boundary!", section.name); return Err("Section not aligned"); } // Check if we can use 2MB blocks (section must be 2MB aligned and sized) - let can_use_2mb = section.phys_start.is_aligned(2 * 1024 * 1024) + let can_use_2mb = section.phys_start.is_aligned(2u64 * 1024 * 1024) && section.virt_start.as_u64() % (2 * 1024 * 1024) == 0 && section.size >= 2 * 1024 * 1024; diff --git a/kernel/nucleus/Cargo.toml b/kernel/nucleus/Cargo.toml index fa3d591db..2a0ee5e82 100644 --- a/kernel/nucleus/Cargo.toml +++ b/kernel/nucleus/Cargo.toml @@ -35,7 +35,7 @@ aarch64-cpu = { workspace = true } bit_field = { workspace = true } bitflags = { workspace = true } cfg-if = { workspace = true } -libboot = { workspace = true } +libaddress = { workspace = true } libconsole = { workspace = true } libcpu = { workspace = true } libexception = { workspace = true } diff --git a/kernel/nucleus/src/api/debug_console.rs b/kernel/nucleus/src/api/debug_console.rs index f8b128ab1..194f40dd7 100644 --- a/kernel/nucleus/src/api/debug_console.rs +++ b/kernel/nucleus/src/api/debug_console.rs @@ -1,6 +1,6 @@ use { crate::{api::KeyEntry, objects::DebugConsole}, - libmemory::phys_addr::PhysAddr, + libaddress::PhysAddr, libobject::{CapError, Key, SyscallResult, debug_console::DebugConsoleOp}, }; diff --git a/kernel/nucleus/src/objects/arch/aarch64_objects.rs b/kernel/nucleus/src/objects/arch/aarch64_objects.rs index a34f785aa..10a666dd5 100644 --- a/kernel/nucleus/src/objects/arch/aarch64_objects.rs +++ b/kernel/nucleus/src/objects/arch/aarch64_objects.rs @@ -11,7 +11,7 @@ use { object_ref::ObjectRef, }, }, - libmemory::{phys_addr::PhysAddr, virt_addr::VirtAddr}, + libaddress::{PhysAddr, VirtAddr}, libobject::{ArchType, CapError, ObjectType, Rights}, }; diff --git a/kernel/nucleus/src/objects/arch/frame.rs b/kernel/nucleus/src/objects/arch/frame.rs index b6fdc98fa..c5ba1833b 100644 --- a/kernel/nucleus/src/objects/arch/frame.rs +++ b/kernel/nucleus/src/objects/arch/frame.rs @@ -4,7 +4,7 @@ use { crate::objects::{NucleusObject, arch_objects::FrameSize}, - libmemory::{phys_addr::PhysAddr, virt_addr::VirtAddr}, + libaddress::{PhysAddr, VirtAddr}, libobject::{CapError, KeySlot, ObjectType}, }; diff --git a/kernel/nucleus/src/objects/arch/page_table.rs b/kernel/nucleus/src/objects/arch/page_table.rs index 64929bd29..8c1dae8b2 100644 --- a/kernel/nucleus/src/objects/arch/page_table.rs +++ b/kernel/nucleus/src/objects/arch/page_table.rs @@ -1,4 +1,4 @@ -use {crate::objects::NucleusObject, libmemory::phys_addr::PhysAddr, libobject::ObjectType}; +use {crate::objects::NucleusObject, libaddress::PhysAddr, libobject::ObjectType}; pub struct AArch64PageTable; diff --git a/kernel/nucleus/src/objects/arch_objects.rs b/kernel/nucleus/src/objects/arch_objects.rs index 50369a250..9f4db15e7 100644 --- a/kernel/nucleus/src/objects/arch_objects.rs +++ b/kernel/nucleus/src/objects/arch_objects.rs @@ -3,7 +3,7 @@ use { api::key_entry::KeyEntry, objects::{NucleusObject, arch::ArchPools, nucleus::Nucleus, object_ref::ObjectRef}, }, - libmemory::phys_addr::PhysAddr, + libaddress::PhysAddr, libobject::{ArchType, CapError, Rights}, }; diff --git a/kernel/nucleus/src/objects/debug_console.rs b/kernel/nucleus/src/objects/debug_console.rs index e6dbc9c97..6e8d6be10 100644 --- a/kernel/nucleus/src/objects/debug_console.rs +++ b/kernel/nucleus/src/objects/debug_console.rs @@ -1,7 +1,7 @@ use { crate::objects::NucleusObject, core::slice, - libmemory::phys_addr::PhysAddr, + libaddress::PhysAddr, libobject::{CapError, ObjectType}, }; diff --git a/kernel/nucleus/src/objects/domain.rs b/kernel/nucleus/src/objects/domain.rs index 85b396653..2d8f24d2d 100644 --- a/kernel/nucleus/src/objects/domain.rs +++ b/kernel/nucleus/src/objects/domain.rs @@ -1,7 +1,7 @@ use { crate::objects::{KeyTable, NucleusObject}, core::{ptr::NonNull, sync::atomic::Ordering}, - libmemory::{phys_addr::PhysAddr, virt_addr::VirtAddr}, + libaddress::{PhysAddr, VirtAddr}, libobject::{ ObjectType, domain::{DcbPage, DomainControlBlock, DomainId, DomainState}, @@ -107,7 +107,7 @@ impl DcbPages { /// Well-known user-space base address for DCB mapping /// This is mapped read-only into all domains - pub const USER_BASE: VirtAddr = VirtAddr::new_unchecked(0x0000_7FFF_FE00_0000); + pub const USER_BASE: VirtAddr = unsafe { VirtAddr::new_unchecked(0x0000_7FFF_FE00_0000) }; /// Create empty DCB pages manager pub const fn new() -> Self { diff --git a/kernel/tests/memory.rs b/kernel/tests/memory.rs index be2c23f64..7c5075a6b 100644 --- a/kernel/tests/memory.rs +++ b/kernel/tests/memory.rs @@ -126,7 +126,7 @@ fn no_manual_mmio_map() { let phys_start_page_addr: PageAddress = PageAddress::from(0); let phys_end_exclusive_page_addr: PageAddress = - phys_start_page_addr.checked_offset(5).unwrap(); + phys_start_page_addr.checked_page_offset(5).unwrap(); let phys_region = MemoryRegion::new(phys_start_page_addr, phys_end_exclusive_page_addr); let num_pages = NonZeroUsize::new(phys_region.num_pages()).unwrap(); @@ -161,11 +161,11 @@ fn translation_table_implementation_sanity() { let virt_start_page_addr: PageAddress = PageAddress::from(0); let virt_end_exclusive_page_addr: PageAddress = - virt_start_page_addr.checked_offset(5).unwrap(); + virt_start_page_addr.checked_page_offset(5).unwrap(); let phys_start_page_addr: PageAddress = PageAddress::from(0); let phys_end_exclusive_page_addr: PageAddress = - phys_start_page_addr.checked_offset(5).unwrap(); + phys_start_page_addr.checked_page_offset(5).unwrap(); let virt_region = MemoryRegion::new(virt_start_page_addr, virt_end_exclusive_page_addr); let phys_region = MemoryRegion::new(phys_start_page_addr, phys_end_exclusive_page_addr); @@ -185,24 +185,27 @@ fn pageaddress_type_method_sanity() { let page_addr: PageAddress = PageAddress::from(KernelGranule::SIZE * 2); assert_eq!( - page_addr.checked_offset(-2), + page_addr.checked_page_offset(-2), Some(PageAddress::::from(0)) ); assert_eq!( - page_addr.checked_offset(2), + page_addr.checked_page_offset(2), Some(PageAddress::::from(KernelGranule::SIZE * 4)) ); assert_eq!( - PageAddress::::from(0).checked_offset(0), + PageAddress::::from(0).checked_page_offset(0), Some(PageAddress::::from(0)) ); - assert_eq!(PageAddress::::from(0).checked_offset(-1), None); + assert_eq!( + PageAddress::::from(0).checked_page_offset(-1), + None + ); let max_page_addr = Address::::new(usize::MAX).align_down_page(); assert_eq!( - PageAddress::::from(max_page_addr).checked_offset(1), + PageAddress::::from(max_page_addr).checked_page_offset(1), None ); diff --git a/libs/address/Cargo.toml b/libs/address/Cargo.toml new file mode 100644 index 000000000..b0058b646 --- /dev/null +++ b/libs/address/Cargo.toml @@ -0,0 +1,41 @@ +[package] +name = "libaddress" + +description = "Vesper nanokernel memory Address types." + +authors = { workspace = true } +categories = { workspace = true } +documentation = { workspace = true } +edition = { workspace = true } +homepage = { workspace = true } +license = { workspace = true } +readme = { workspace = true } +repository = { workspace = true } +version = { workspace = true } + +publish = false + +[badges] +maintenance = { status = "experimental" } + +[dependencies] +aarch64-cpu = { workspace = true } +bit_field = { workspace = true } +bitfield-struct = { workspace = true } +bitflags = { workspace = true } +buddy-alloc = { workspace = true } +cfg-if = { workspace = true } +liblocking = { workspace = true } +liblog = { workspace = true } +num = { workspace = true } +once_cell = { workspace = true } +snafu = { workspace = true } +tock-registers = { workspace = true } +usize_conversions = { workspace = true } +ux = { workspace = true } + +[lib] +test = false + +[lints] +workspace = true diff --git a/libs/address/README.md b/libs/address/README.md new file mode 100644 index 000000000..abe6fe26c --- /dev/null +++ b/libs/address/README.md @@ -0,0 +1,18 @@ +# libaddress + +The types Address and Address represent the addresses before and after the mapping in the MMU. + +## Exports + +This library should export the following: + +- Type `Address` representing the physical memory address, independent of the target platform + (plus some shared operations like NumOps). +- Type `PhysAddr` specific to the target platform, with size and content limitations. +- Type `Address` representing the virtual memory address, independent of the target platform +- plus some shared operations like NumOps or ToPointer/FromPointer ops. +- Type `VirtAddr` specific to the target platform (e.g. with_asid() on aarch64) + +---- + +For more information please re-read. diff --git a/libs/address/src/align.rs b/libs/address/src/align.rs new file mode 100644 index 000000000..c3ec2cd6e --- /dev/null +++ b/libs/address/src/align.rs @@ -0,0 +1,79 @@ +/// Align address downwards. +/// +/// Returns the greatest x with alignment `align` so that x <= addr. +/// The alignment must be a power of 2. +#[inline(always)] +pub const fn align_down_bits(addr: u64, alignment_bits: u64) -> u64 { + addr & !((1 << alignment_bits) - 1) +} + +/// Align address downwards. +/// +/// Returns the greatest x with alignment `align` so that x <= addr. +/// The alignment must be a power of 2. +#[inline(always)] +pub const fn align_down(addr: u64, alignment: u64) -> u64 { + assert!( + alignment.is_power_of_two(), + "`alignment` must be a power of two" + ); + addr & !(alignment - 1) +} + +/// Align address upwards. +/// +/// Returns the smallest x with alignment `align` so that x >= addr. +/// The alignment must be a power of 2. +#[inline(always)] +pub const fn align_up_bits(value: u64, alignment_bits: u64) -> u64 { + let align_mask = (1 << alignment_bits) - 1; + if value & align_mask == 0 { + value // already aligned + } else { + (value | align_mask) + 1 + } +} + +/// Align address upwards. +/// +/// Returns the smallest x with alignment `align` so that x >= addr. +/// The alignment must be a power of 2. +#[inline(always)] +pub const fn align_up(value: u64, alignment: u64) -> u64 { + assert!( + alignment.is_power_of_two(), + "`alignment` must be a power of two" + ); + + let align_mask = alignment - 1; + if value & align_mask == 0 { + value // already aligned + } else { + (value | align_mask) + 1 + } +} + +/// Check if a value is aligned to a given alignment. +#[inline(always)] +pub const fn is_aligned_bits(value: u64, alignment_bits: u64) -> bool { + (value & ((1 << alignment_bits) - 1)) == 0 +} + +/// Check if a value is aligned to a given alignment. +/// The alignment must be a power of 2. +#[inline(always)] +pub const fn is_aligned(value: u64, alignment: u64) -> bool { + debug_assert!( + alignment.is_power_of_two(), + "`alignment` must be a power of two" + ); + + (value & (alignment - 1)) == 0 +} + +/// Calculate the next possible aligned address without sanity checking the +/// input parameters. +#[inline] +pub fn aligned_addr_unchecked(addr: u64, alignment: u64) -> u64 { + (addr + (alignment - 1)) & !(alignment - 1) +} diff --git a/libs/address/src/lib.rs b/libs/address/src/lib.rs new file mode 100644 index 000000000..ba7c99f41 --- /dev/null +++ b/libs/address/src/lib.rs @@ -0,0 +1,613 @@ +// +// SPDX-License-Identifier: BlueOak-1.0.0 +// Copyright (c) Berkus Decker +// + +#![no_std] +#![feature(const_trait_impl)] +#![feature(const_ops)] +#![feature(const_convert)] + +use { + // bit_field::BitField, + bitfield_struct::bitfield, + core::{ + convert::{From, TryInto}, + fmt, + marker::PhantomData, + ops::{Add, AddAssign, Rem, RemAssign, Shl, Shr, Sub, SubAssign}, + }, + usize_conversions::FromUsize, + ux::*, +}; + +//-------------------------------------------------------------------------------------------------- +// Public Definitions +//-------------------------------------------------------------------------------------------------- + +pub mod align; + +/// A 64-bit physical memory address. +/// +/// This is a wrapper type around an `u64`, so it is always 8 bytes, even when compiled +/// on non 64-bit systems. The `UsizeConversions` trait can be used for performing conversions +/// between `u64` and `usize`. +/// +/// On `aarch64`, only the 52 lower bits of a physical address can be used. The top 12 bits need +/// to be zero. This type guarantees that it always represents a valid physical address. +pub type PhysAddr = Address; + +/// A canonical 64-bit virtual memory address. +/// +/// This is a wrapper type around an `u64`, so it is always 8 bytes, even when compiled +/// on non 64-bit systems. The `UsizeConversions` trait can be used for performing conversions +/// between `u64` and `usize`. +/// +/// On `x86_64`, only the 48 lower bits of a virtual address can be used. The top 16 bits need +/// to be copies of bit 47, i.e. the most significant bit. Addresses that fulfil this criterium +/// are called “canonical”. This type guarantees that it always represents a canonical address. +pub type VirtAddr = Address; + +/// Address of all physical memory mapping, so that kernel can operate anywhere. +pub const PHYSICAL_KERNEL_WINDOW: u64 = 0xffff_f000_0000_0000; + +/// Metadata trait for marking the type of an address. +pub const trait AddressType: Copy + Clone + PartialOrd + PartialEq + Ord + Eq { + const NAME: &'static str; + fn validate(addr: u64) -> Result; // Ok(canonical_addr) or Err(raw, reason) +} + +pub const trait PageSize { + fn alignment(&self) -> u64; + fn mask(&self) -> u64; +} + +impl const PageSize for u64 { + fn alignment(&self) -> u64 { + *self + } + fn mask(&self) -> u64 { + !(self - 1) + } +} + +impl const PageSize for usize { + fn alignment(&self) -> u64 { + *self as u64 + } + fn mask(&self) -> u64 { + !(self - 1) as u64 + } +} + +/// Zero-sized type to mark a physical address. +#[derive(Copy, Clone, Debug, PartialOrd, PartialEq, Ord, Eq)] +pub enum Physical {} + +/// Zero-sized type to mark a virtual address. +#[derive(Copy, Clone, Debug, PartialOrd, PartialEq, Ord, Eq)] +pub enum Virtual {} + +/// Generic address type. +/// +/// This is a wrapper type around an `u64`, so it is always 8 bytes, even when compiled +/// on non 64-bit systems. The `UsizeConversions` trait can be used for performing conversions +/// between `u64` and `usize`. +#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord)] +#[repr(transparent)] +pub struct Address { + pub(crate) value: u64, + pub(crate) _address_type: PhantomData ATYPE>, +} + +const _: () = assert!(core::mem::size_of::>() == core::mem::size_of::()); +const _: () = assert!(core::mem::size_of::>() == core::mem::size_of::()); + +/// A passed `u64` was not a valid address. +/// +/// What this means exactly depends on architecture assumptions. +/// Arch crate is expected to implement this trait and provide +/// additional information about why this address is invalid. +#[derive(Default, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Debug)] +pub struct AddressNotValid { + pub(crate) value: u64, + pub(crate) _address_type: PhantomData ATYPE>, +} + +//-------------------------------------------------------------------------------------------------- +// Public Code +//-------------------------------------------------------------------------------------------------- + +//================================================================================================= +// AddressNotValid +//================================================================================================= + +impl AddressNotValid { + pub const fn new(value: u64) -> Self { + Self { + value, + _address_type: PhantomData, + } + } +} + +//================================================================================================= +// Physical/Virtual +//================================================================================================= + +#[bitfield(u64)] +struct PhysTopBits { + #[bits(52)] + address: i64, + #[bits(12)] // bits(52..64) + top_bits: u16, +} + +impl const AddressType for Physical { + const NAME: &'static str = "PhysAddr"; + + /// Panics if any bits in the bit position 52 to 64 is set. + /// TODO: this is arch-dependent!. + fn validate(addr: u64) -> Result { + match PhysTopBits(addr).top_bits() { + 0 => Ok(addr), + _ => Err(( + addr, + "physical addresses must not have any set bits in positions 52 to 64", + )), + } + } +} + +#[bitfield(u64)] +struct VirtTopBits { + #[bits(47)] + address: i64, + #[bits(17)] // bits(47..64) + top_bits: u32, +} + +impl const AddressType for Virtual { + const NAME: &'static str = "VirtAddr"; + + /// This function tries to performs sign extension of bit 47 to make the address canonical. + /// It succeeds if bits 47 to 64 are a correct sign extension or all null (i.e. copies of bit 47). + /// + /// An error means that bits 48 to 64 are not a valid sign extension and are not null either. + /// So automatic sign extension would have overwritten possibly meaningful bits. + /// This likely indicates a bug, for example an invalid address calculation. + /// TODO: Support ASID byte in top bits of the address. + fn validate(addr: u64) -> Result { + match VirtTopBits(addr).top_bits() { + 0 | 0x1ffff => Ok(addr), // address is canonical + 1 => { + // address needs sign extension + let mut addr = VirtTopBits(addr); + addr.set_top_bits(0x1ffff); + Ok(addr.into_bits()) + } + _ => Err(( + addr, + "virtual address must not contain any data in bits 48 to 64", + )), + } + } +} + +//================================================================================================= +// Address +//================================================================================================= + +impl Default for Address { + fn default() -> Self { + Self::zero() + } +} + +impl Address { + /// Create an address without checking. + /// # Safety + pub const unsafe fn new_unchecked(value: u64) -> Self { + Self { + value, + _address_type: PhantomData, + } + } + + /// Creates a new address. + /// + /// Panics if address is not representable. + pub const fn new(addr: u64) -> Self { + match ATYPE::validate(addr) { + Ok(addr) => unsafe { Address::::new_unchecked(addr) }, + Err((_addr, message)) => panic!("{}", message), + } + } + + /// Tries to create a new address. + pub const fn try_new(addr: u64) -> Result> { + match ATYPE::validate(addr) { + Ok(addr) => Ok(unsafe { Address::::new_unchecked(addr) }), + Err((addr, _message)) => Err(AddressNotValid::::new(addr)), + } + } + + /// Converts the address to an `u64`. + pub const fn as_u64(&self) -> u64 { + self.value + } + + pub fn as_usize(&self) -> usize { + self.value.try_into().unwrap() + } + + /// Creates an address that points to `0`. + pub const fn zero() -> Address { + Self { + value: 0, + _address_type: PhantomData, + } + } + + /// Converts the address to a raw pointer. + pub const fn into_ptr(self) -> *const T { + self.value as *const T + } + + /// Converts the address to a raw pointer. + pub const fn as_ptr(&self) -> *const T { + self.value as *const T + } + + /// Converts the address to a mutable raw pointer. + pub const fn into_mut_ptr(self) -> *mut T { + self.value as *mut T + } + + /// Converts the address to a mutable raw pointer. + pub const fn as_mut_ptr(&self) -> *mut T { + self.value as *mut T + } + + /// Convenience method for checking if a physical address is null. + pub fn is_null(&self) -> bool { + self.value == 0 + } + + /// Creates an address from the given pointer + pub fn from_ptr(ptr: *const T) -> Self { + Self::new(u64::from_usize(ptr as usize)) + } + + /// Creates a virtual address from the given pointer + pub fn from_mut_ptr(ptr: *mut T) -> Self { + Self::new(u64::from_usize(ptr as usize)) + } + + // TODO: With pageSize parameterized we can move it out to platform-independent code. + + /// Align down to page size. + #[must_use] + pub const fn align_down_page(&self, page_size: &impl const PageSize) -> Self { + let aligned = align::align_down(self.value, page_size.alignment()); + Self::new(aligned) + } + + /// Align up to page size. + #[must_use] + pub const fn align_up_page(&self, page_size: &impl const PageSize) -> Self { + let aligned = align::align_up(self.value, page_size.alignment()); + Self::new(aligned) + } + + /// Checks if the address is page aligned. + pub const fn is_page_aligned(&self, page_size: &impl const PageSize) -> bool { + align::is_aligned(self.value, page_size.alignment()) + } + + /// Return the address' offset into the corresponding page. + pub const fn offset_into_page(&self, page_size: &impl const PageSize) -> u64 { + self.value & page_size.mask() + } + + /// Aligns the address upwards to the given alignment. + /// + /// See the `align_up` function for more information. + #[must_use] + pub fn aligned_up(self, align: U) -> Self + where + U: Into, + { + Self { + value: align::align_up(self.value, align.into()), + _address_type: PhantomData, + } + } + + /// Aligns the address downwards to the given alignment. + /// + /// See the `align_down` function for more information. + #[must_use] + pub fn aligned_down(self, align: U) -> Self + where + U: Into, + { + Self { + value: align::align_down(self.value, align.into()), + _address_type: PhantomData, + } + } + + /// Checks whether the address has the required alignment. + pub fn is_aligned>(self, align: U) -> bool { + align::is_aligned(self.value, align.into()) + } +} + +//================================================================================================= +// Address specifics +//================================================================================================= + +impl Address { + /// Convert physical memory address into a kernel-view virtual address for physical memory. + pub fn user_to_kernel(&self) -> Address { + assert!(self.value < !PHYSICAL_KERNEL_WINDOW); + Address::::new(self.value + PHYSICAL_KERNEL_WINDOW) + } +} + +//================================================================================================= +// Address specifics +//================================================================================================= + +impl Address { + /// Creates a new canonical virtual address without checks (overwriting top bits). + /// + /// This function performs sign extension of bit 47 to make the address canonical, so + /// bits 48 to 64 are overwritten. If you want to check that these bits contain no data, + /// use `new` or `try_new`. + pub const fn new_canonical(addr: u64) -> Self { + let mut v = VirtTopBits(addr); + if v.top_bits() & 1 != 0 { + v.set_top_bits(0x1ffff); + } else { + v.set_top_bits(0); + } + unsafe { Self::new_unchecked(v.into_bits()) } + } + + // @todo Support ASID byte in top bits of the address. + // pub fn with_asid(addr: u64, asid: ASID) -> Address {} + + // TODO: The following index and page fns should be accessible through a PageSize trait or something? + + /// Returns the 12-bit page offset of this virtual address. + pub fn page_offset(&self) -> u12 { + u12::new((self.value & 0xfff).try_into().unwrap()) + } + // ^ @todo this only works for 4KiB pages + + /// Returns the 9-bit level 3 page table index. + pub fn l3_index(&self) -> u9 { + u9::new(((self.value >> 12) & 0o777).try_into().unwrap()) + } + + /// Returns the 9-bit level 2 page table index. + pub fn l2_index(&self) -> u9 { + u9::new(((self.value >> 12 >> 9) & 0o777).try_into().unwrap()) + } + + /// Returns the 9-bit level 1 page table index. + pub fn l1_index(&self) -> u9 { + u9::new(((self.value >> 12 >> 9 >> 9) & 0o777).try_into().unwrap()) + } + + /// Returns the 9-bit level 0 page table index. + pub fn l0_index(&self) -> u9 { + u9::new( + ((self.value >> 12 >> 9 >> 9 >> 9) & 0o777) + .try_into() + .unwrap(), + ) + } + + pub const fn is_higher_half(self) -> bool { + self.value >= 0xFFFF_0000_0000_0000 + } + + /// Convert kernel-view virtual address of physical memory into a physical memory address. + pub fn kernel_to_user(&self) -> Address { + assert!(self.value >= PHYSICAL_KERNEL_WINDOW); + Address::::new(self.value - PHYSICAL_KERNEL_WINDOW) + } +} + +//================================================================================================= +// From +//================================================================================================= + +impl From for Address { + fn from(value: u64) -> Self { + Self::new(value) + } +} + +impl From for Address { + fn from(value: usize) -> Self { + Self::new(value as u64) + } +} + +impl From> for u64 { + fn from(value: Address) -> Self { + value.as_u64() + } +} + +impl From> for u128 { + fn from(value: Address) -> Self { + u128::from(value.as_u64()) + } +} + +//================================================================================================= +// Add/AddAssign +//================================================================================================= + +impl Add for Address { + type Output = Self; + + /// Add a given offset to the current virtual address. Never wraps. + #[inline(always)] + fn add(self, rhs: T) -> Self::Output { + // @todo runtime cost of unwrap() here + // VirtAddr::new(self.value.saturating_add(num::cast(rhs).unwrap())) + match self.value.checked_add(num::cast(rhs).unwrap()) { + None => panic!("Overflow on Address::add"), + Some(x) => Self::new(x), + } + } +} + +// FIXME: this is already a default impl? +impl AddAssign for Address { + fn add_assign(&mut self, rhs: T) { + *self = *self + rhs; + } +} + +//================================================================================================= +// Sub/SubAssign +//================================================================================================= + +// Difference of two addresses is a size. +impl Sub> for Address { + type Output = usize; + + fn sub(self, rhs: Address) -> Self::Output { + match self.value.checked_sub(rhs.value) { + None => panic!("Overflow on Address::sub"), + Some(x) => x as usize, + } + } +} + +impl Sub for Address { + type Output = Self; + + fn sub(self, rhs: T) -> Self::Output { + Address::::new(self.value.checked_sub(num::cast(rhs).unwrap()).unwrap()) + } +} + +impl SubAssign for Address { + fn sub_assign(&mut self, rhs: T) { + *self = *self - rhs; + } +} + +//================================================================================================= +// Shr/Shl +//================================================================================================= + +impl Shr for Address { + type Output = Self; + + fn shr(self, shift: usize) -> Self::Output { + Address::::new(self.value >> shift) + } +} + +impl Shl for Address { + type Output = Self; + + fn shl(self, shift: usize) -> Self::Output { + Address::::new(self.value << shift) + } +} + +//================================================================================================= +// Rem/RemAssign +//================================================================================================= + +impl Rem for Address { + type Output = u64; + + fn rem(self, rhs: T) -> Self::Output { + num::traits::CheckedRem::checked_rem(&self.value, &num::cast(rhs).unwrap()).unwrap() + } +} + +// @todo this is not very useful... +impl RemAssign for Address { + fn rem_assign(&mut self, rhs: T) { + *self = Address::::new( + num::traits::CheckedRem::checked_rem(&self.value, &num::cast(rhs).unwrap()).unwrap(), + ); + } +} + +//================================================================================================= +// Display/Debug/fmt +//================================================================================================= + +impl fmt::Debug for Address { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}({:#x})", ATYPE::NAME, self.value) + } +} + +impl fmt::Display for Address { + // Don't expect to see physical addresses greater than 40 bit. + #[allow(clippy::cast_possible_truncation)] + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + let q3: u8 = ((self.value >> 32) & 0xff) as u8; + let q2: u16 = ((self.value >> 16) & 0xffff) as u16; + let q1: u16 = (self.value & 0xffff) as u16; + + write!(f, "pa")?; + write!(f, "{q3:02x}_")?; + write!(f, "{q2:04x}_")?; + write!(f, "{q1:04x}") + } +} + +impl fmt::Display for Address { + #[allow(clippy::cast_possible_truncation)] + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + let q4: u16 = ((self.value >> 48) & 0xffff) as u16; + let q3: u16 = ((self.value >> 32) & 0xffff) as u16; + let q2: u16 = ((self.value >> 16) & 0xffff) as u16; + let q1: u16 = (self.value & 0xffff) as u16; + + write!(f, "va")?; + write!(f, "{q4:04x}_")?; + write!(f, "{q3:04x}_")?; + write!(f, "{q2:04x}_")?; + write!(f, "{q1:04x}") + } +} + +impl fmt::Binary for Address { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + self.value.fmt(f) + } +} + +impl fmt::LowerHex for Address { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + self.value.fmt(f) + } +} + +impl fmt::UpperHex for Address { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + self.value.fmt(f) + } +} + +impl fmt::Octal for Address { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + self.value.fmt(f) + } +} diff --git a/libs/memory/test/memory_test.rs b/libs/address/test/memory_test.rs similarity index 100% rename from libs/memory/test/memory_test.rs rename to libs/address/test/memory_test.rs diff --git a/libs/boot/Cargo.toml b/libs/boot/Cargo.toml index 6be6aadf2..1955df528 100644 --- a/libs/boot/Cargo.toml +++ b/libs/boot/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "libboot" -description = "Kernel boot code" +description = "Vesper nanokernel boot PIE section." authors = { workspace = true } categories = { workspace = true } diff --git a/libs/boot/src/arch/aarch64/boot.rs b/libs/boot/src/arch/aarch64/boot.rs index 10de2a2ed..c675b21c1 100644 --- a/libs/boot/src/arch/aarch64/boot.rs +++ b/libs/boot/src/arch/aarch64/boot.rs @@ -97,7 +97,7 @@ pub unsafe extern "C" fn _startup_in_rust(dtb: u32) -> ! { // Set Hypervisor Configuration Register (EL2) // Set EL1 execution state to AArch64 - // @todo Explain the SWIO bit (SWIO hardwired on Pi3) + // @todo Explain the SWIO bit (SWIO is hardwired on RPi3) HCR_EL2.write(HCR_EL2::RW::EL1IsAarch64 + HCR_EL2::SWIO::SET); // @todo disable VM bit to prevent stage 2 MMU translations @@ -125,10 +125,10 @@ pub unsafe extern "C" fn _startup_in_rust(dtb: u32) -> ! { #[unsafe(link_section = ".text.boot")] #[inline] fn setup_and_enter_el2_from_el3(dtb: u32) -> ! { - // Set Secure Configuration Register (EL3) + // Set Secure Configuration Register in EL3. SCR_EL3.write(SCR_EL3::RW::NextELIsAarch64 + SCR_EL3::NS::NonSecure); - // Set Saved Program Status Register (EL3) + // Set Saved Program Status Register in EL3. // Set up a simulated exception return. // // Fake a saved program status, where all interrupts were @@ -141,7 +141,7 @@ fn setup_and_enter_el2_from_el3(dtb: u32) -> ! { + SPSR_EL3::M::EL2h, // Use SP_EL2 ); - // Make the Exception Link Register (EL3) point to reset(). + // Make the Exception Link Register in EL3 point to reset(). ELR_EL3.set(reset as *const () as u64); unsafe extern "Rust" { diff --git a/libs/memory/Cargo.toml b/libs/memory/Cargo.toml index 4b5bf3ab6..95ed34171 100644 --- a/libs/memory/Cargo.toml +++ b/libs/memory/Cargo.toml @@ -24,6 +24,7 @@ bit_field = { workspace = true } bitflags = { workspace = true } buddy-alloc = { workspace = true } cfg-if = { workspace = true } +libaddress = { workspace = true } liblocking = { workspace = true } liblog = { workspace = true } num = { workspace = true } diff --git a/libs/memory/README.md b/libs/memory/README.md new file mode 100644 index 000000000..b3f8824dd --- /dev/null +++ b/libs/memory/README.md @@ -0,0 +1,19 @@ +# libmemory + +libmemory contains only types (structs and traits) and abstractions useful for memory translation. +It includes memory mapping tables layout and MMU interface. + +## Exports + +Page table types must represent pages of differing sizes. +For every entry in the MMU page table we should be able to receive a proper page type - e.g. Invalid, further page table, or a specific-size page. + +This library should export the following: + +- MMU control interface under `interface::MMU` +- Page-table hierarchy representation. This needs to be platform-independent. + Abstract translation stages and page size granularity. + +--- + +For more information please re-read. diff --git a/libs/memory/src/arch/aarch64/addr/docs.md b/libs/memory/src/arch/aarch64/addr/docs.md new file mode 100644 index 000000000..e4e11a05b --- /dev/null +++ b/libs/memory/src/arch/aarch64/addr/docs.md @@ -0,0 +1,12 @@ +## Arch-independent + +Address is a 64-bit unsigned number (_address_ of a location in memory) + +It can be aligned and checked for alignment. +There are not other limitations on the address. + +## Arch-dependent + +Address may have a size limitation, e.g. a 40-bits physical address on some platforms. - could be platform-dependent, not arch-dependent! + +Address may contain additional information payload, for example ASID tags. diff --git a/libs/memory/src/arch/aarch64/mmu/mod.rs b/libs/memory/src/arch/aarch64/mmu/mod.rs index 10d5cfeb0..37f8370b3 100644 --- a/libs/memory/src/arch/aarch64/mmu/mod.rs +++ b/libs/memory/src/arch/aarch64/mmu/mod.rs @@ -1,6 +1,5 @@ use { crate::{ - Address, Physical, arch::mmu::translation_table::{ PageFlags, PageSize, STAGE1_PAGE_DESCRIPTOR, STAGE1_TABLE_DESCRIPTOR, Size2MiB, Size4KiB, TableFlags, @@ -12,6 +11,7 @@ use { registers::{ID_AA64MMFR0_EL1, SCTLR_EL1, TCR_EL1}, }, core::intrinsics::unlikely, + libaddress::{Address, Physical}, liblog::println, tock_registers::interfaces::{ReadWriteable, Readable, Writeable}, }; diff --git a/libs/memory/src/arch/aarch64/mmu/translation_table.rs b/libs/memory/src/arch/aarch64/mmu/translation_table.rs index 326095045..ef685eb93 100644 --- a/libs/memory/src/arch/aarch64/mmu/translation_table.rs +++ b/libs/memory/src/arch/aarch64/mmu/translation_table.rs @@ -6,11 +6,11 @@ use core::{ use { super::{Granule64KiB, Granule512MiB, mair}, crate::{ - Address, Physical, Virtual, mmu::{AccessPermissions, AttributeFields, MemAttributes, MemoryRegion, PageAddress}, platform, }, core::convert, + libaddress::{Address, Physical, Virtual}, tock_registers::{ interfaces::{Readable, Writeable}, register_bitfields, @@ -146,9 +146,6 @@ pub struct FixedSizeTranslationTable { initialized: bool, } -// /// A translation table type for the kernel space. -// pub type KernelTranslationTable = FixedSizeTranslationTable; - //-------------------------------------------------------------------------------------------------- // Private Implementations //-------------------------------------------------------------------------------------------------- @@ -156,7 +153,7 @@ pub struct FixedSizeTranslationTable { impl BaseAddr for [T; N] { // The binary is still identity mapped, so we don't need to convert here. fn phys_start_addr(&self) -> Address { - Address::new(core::ptr::from_ref(self) as usize) + Address::from_ptr(core::ptr::from_ref(self)) } fn base_addr_u64(&self) -> u64 { @@ -172,7 +169,7 @@ impl TableDescriptor { /// Create an instance. /// /// Descriptor is invalid by default. - pub const fn new_zeroed() -> Self { + pub const fn zeroed() -> Self { Self { value: 0 } } @@ -195,7 +192,7 @@ impl PageDescriptor { /// Create an instance. /// /// Descriptor is invalid by default. - pub const fn new_zeroed() -> Self { + pub const fn zeroed() -> Self { Self { value: 0 } } @@ -295,8 +292,8 @@ impl FixedSizeTranslationTable { Self { #[allow(clippy::large_stack_arrays)] - lvl3: [[PageDescriptor::new_zeroed(); 8192]; NUM_TABLES], - lvl2: [TableDescriptor::new_zeroed(); NUM_TABLES], + lvl3: [[PageDescriptor::zeroed(); 8192]; NUM_TABLES], + lvl2: [TableDescriptor::zeroed(); NUM_TABLES], initialized: false, } } diff --git a/libs/memory/src/arch/aarch64/mod.rs b/libs/memory/src/arch/aarch64/mod.rs index b00861957..f8a99ef6f 100644 --- a/libs/memory/src/arch/aarch64/mod.rs +++ b/libs/memory/src/arch/aarch64/mod.rs @@ -18,7 +18,7 @@ mod virt_page; // use self::paging::PAGE_SIZE; // pub use crate::memory::{PhysAddr, VirtAddr}; -pub use {page_size::PageSize, phys_frame::PhysFrame}; +pub use phys_frame::PhysFrame; /// @todo ?? pub trait FrameAllocator { diff --git a/libs/memory/src/arch/aarch64/phys_frame.rs b/libs/memory/src/arch/aarch64/phys_frame.rs index 718d3a6e3..b6c583516 100644 --- a/libs/memory/src/arch/aarch64/phys_frame.rs +++ b/libs/memory/src/arch/aarch64/phys_frame.rs @@ -3,12 +3,12 @@ use { super::page_size::{PageSize, Size4KiB}, - crate::phys_addr::PhysAddr, core::{ fmt, marker::PhantomData, ops::{Add, AddAssign, Sub, SubAssign}, }, + libaddress::PhysAddr, }; /// A physical memory frame. @@ -37,7 +37,7 @@ impl PhysFrame { /// /// Returns an error if the address is not correctly aligned (i.e. is not a valid frame start). pub fn from_start_address(address: PhysAddr) -> Result { - if !address.is_aligned(S::SIZE) { + if !address.is_aligned(S::SIZE as u64) { return Err(()); } Ok(PhysFrame::containing_address(address)) @@ -46,7 +46,7 @@ impl PhysFrame { /// Returns the frame that contains the given physical address. pub fn containing_address(address: PhysAddr) -> Self { PhysFrame { - start_address: address.aligned_down(S::SIZE), + start_address: address.aligned_down(S::SIZE as u64), size: PhantomData, } } diff --git a/libs/memory/src/arch/aarch64/virt_page.rs b/libs/memory/src/arch/aarch64/virt_page.rs index fc488d173..9f62690be 100644 --- a/libs/memory/src/arch/aarch64/virt_page.rs +++ b/libs/memory/src/arch/aarch64/virt_page.rs @@ -7,15 +7,13 @@ #![allow(dead_code)] use { - crate::{ - arch::aarch64::page_size::{NotGiantPageSize, PageSize, Size1GiB, Size2MiB, Size4KiB}, - virt_addr::VirtAddr, - }, + crate::arch::aarch64::page_size::{NotGiantPageSize, PageSize, Size1GiB, Size2MiB, Size4KiB}, core::{ fmt, marker::PhantomData, ops::{Add, AddAssign, Sub, SubAssign}, }, + libaddress::VirtAddr, ux::u9, }; @@ -38,7 +36,7 @@ impl Page { /// /// Returns an error if the address is not correctly aligned (i.e. is not a valid page start). pub fn from_start_address(address: VirtAddr) -> Result { - if !address.is_aligned(S::SIZE) { + if !address.is_aligned(S::SIZE as u64) { Err(Error::NotAligned) } else { Ok(Page::containing_address(address)) @@ -48,7 +46,7 @@ impl Page { /// Returns the page that contains the given virtual address. pub fn containing_address(address: VirtAddr) -> Self { Page { - start_address: address.aligned_down(S::SIZE), + start_address: address.aligned_down(S::SIZE as u64), size: PhantomData, } } diff --git a/libs/memory/src/lib.rs b/libs/memory/src/lib.rs index c33206cab..d46f6bbac 100644 --- a/libs/memory/src/lib.rs +++ b/libs/memory/src/lib.rs @@ -8,6 +8,7 @@ #![allow(dead_code)] // while refactoring #![allow(incomplete_features)] #![feature(generic_const_exprs)] // incomplete_features +#![feature(const_trait_impl)] #![feature(format_args_nl)] #![allow(internal_features)] #![feature(allocator_api)] @@ -15,143 +16,24 @@ #![feature(step_trait)] #![feature(custom_test_frameworks)] -use core::{ - fmt, - marker::PhantomData, - ops::{Add, Sub}, -}; - -pub mod arch; +mod arch; pub mod mm; pub mod mmu; pub mod platform; -pub mod phys_addr; // merge with Address? -pub mod virt_addr; // merge with Address? - -//-------------------------------------------------------------------------------------------------- -// Public Definitions -//-------------------------------------------------------------------------------------------------- - -/// Metadata trait for marking the type of an address. -pub trait AddressType: Copy + Clone + PartialOrd + PartialEq + Ord + Eq {} - -/// Zero-sized type to mark a physical address. -#[derive(Copy, Clone, Debug, PartialOrd, PartialEq, Ord, Eq)] -pub enum Physical {} - -/// Zero-sized type to mark a virtual address. -#[derive(Copy, Clone, Debug, PartialOrd, PartialEq, Ord, Eq)] -pub enum Virtual {} - -/// Generic address type. -#[derive(Copy, Clone, Debug, PartialOrd, PartialEq, Ord, Eq)] -pub struct Address { - value: usize, - _address_type: PhantomData ATYPE>, -} - -//-------------------------------------------------------------------------------------------------- -// Public Code -//-------------------------------------------------------------------------------------------------- - -impl AddressType for Physical {} -impl AddressType for Virtual {} - -impl Address { - /// Create an instance. - pub const fn new(value: usize) -> Self { - Self { - value, - _address_type: PhantomData, - } - } - - /// Convert to usize. - pub const fn as_usize(self) -> usize { - self.value - } - - /// Align down to page size. - #[must_use] - pub const fn align_down_page(self) -> Self { - let aligned = mm::align_down(self.value, platform::KernelGranule::SIZE); - - Self::new(aligned) - } - - /// Align up to page size. - #[must_use] - pub const fn align_up_page(self) -> Self { - let aligned = mm::align_up(self.value, platform::KernelGranule::SIZE); - - Self::new(aligned) - } - - /// Checks if the address is page aligned. - pub const fn is_page_aligned(&self) -> bool { - mm::is_aligned(self.value, platform::KernelGranule::SIZE) - } - - /// Return the address' offset into the corresponding page. - pub const fn offset_into_page(&self) -> usize { - self.value & platform::KernelGranule::MASK - } -} - -impl Add for Address { - type Output = Self; - - #[inline(always)] - fn add(self, rhs: usize) -> Self::Output { - match self.value.checked_add(rhs) { - None => panic!("Overflow on Address::add"), - Some(x) => Self::new(x), - } - } -} - -impl Sub> for Address { - type Output = Self; - - #[inline(always)] - fn sub(self, rhs: Address) -> Self::Output { - match self.value.checked_sub(rhs.value) { - None => panic!("Overflow on Address::sub"), - Some(x) => Self::new(x), - } +/// Convert a size into human readable format. +pub const fn size_human_readable_ceil(size: usize) -> (usize, &'static str) { + const KIB: usize = 1024; + const MIB: usize = 1024 * 1024; + const GIB: usize = 1024 * 1024 * 1024; + + if (size / GIB) > 0 { + (size.div_ceil(GIB), "GiB") + } else if (size / MIB) > 0 { + (size.div_ceil(MIB), "MiB") + } else if (size / KIB) > 0 { + (size.div_ceil(KIB), "KiB") + } else { + (size, "Byte") } } - -impl fmt::Display for Address { - // Don't expect to see physical addresses greater than 40 bit. - #[allow(clippy::cast_possible_truncation)] - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - let q3: u8 = ((self.value >> 32) & 0xff) as u8; - let q2: u16 = ((self.value >> 16) & 0xffff) as u16; - let q1: u16 = (self.value & 0xffff) as u16; - - write!(f, "0x")?; - write!(f, "{q3:02x}_")?; - write!(f, "{q2:04x}_")?; - write!(f, "{q1:04x}") - } -} - -impl fmt::Display for Address { - #[allow(clippy::cast_possible_truncation)] - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - let q4: u16 = ((self.value >> 48) & 0xffff) as u16; - let q3: u16 = ((self.value >> 32) & 0xffff) as u16; - let q2: u16 = ((self.value >> 16) & 0xffff) as u16; - let q1: u16 = (self.value & 0xffff) as u16; - - write!(f, "0x")?; - write!(f, "{q4:04x}_")?; - write!(f, "{q3:04x}_")?; - write!(f, "{q2:04x}_")?; - write!(f, "{q1:04x}") - } -} - -pub const PHYSICAL_KERNEL_WINDOW: u64 = 0xffff_f000_0000_0000; diff --git a/libs/memory/src/mm/bump_allocator.rs b/libs/memory/src/mm/bump_allocator.rs index d774cf33a..567659031 100644 --- a/libs/memory/src/mm/bump_allocator.rs +++ b/libs/memory/src/mm/bump_allocator.rs @@ -27,15 +27,18 @@ unsafe impl Allocator for BumpAllocator { fn allocate(&self, layout: Layout) -> Result, AllocError> { let name = self.name; let size = layout.size(); - let start = crate::mm::aligned_addr_unchecked(self.next.get(), layout.align()); - let end = start + layout.size(); + let start = libaddress::align::aligned_addr_unchecked( + self.next.get() as u64, + layout.align() as u64, + ); + let end = start + layout.size() as u64; println!("[i] {name}:\n Allocating Start {start:#010x} End {end:#010x}",); - if end > self.pool_end { + if end > self.pool_end as u64 { return Err(AllocError); } - self.next.set(end); + self.next.set(end.try_into().unwrap()); println!("[i] {name}:\n Allocated Addr {start:#010x} Size {size:#x}",); diff --git a/libs/memory/src/mm/mod.rs b/libs/memory/src/mm/mod.rs index f328c83af..0e5257500 100644 --- a/libs/memory/src/mm/mod.rs +++ b/libs/memory/src/mm/mod.rs @@ -5,71 +5,3 @@ mod bump_allocator; pub use bump_allocator::BumpAllocator; - -/// Align address downwards. -/// -/// Returns the greatest x with alignment `align` so that x <= addr. -/// The alignment must be a power of 2. -#[inline(always)] -pub const fn align_down(addr: usize, alignment: usize) -> usize { - assert!( - alignment.is_power_of_two(), - "`alignment` must be a power of two" - ); - addr & !(alignment - 1) -} - -/// Align address upwards. -/// -/// Returns the smallest x with alignment `align` so that x >= addr. -/// The alignment must be a power of 2. -#[inline(always)] -pub const fn align_up(value: usize, alignment: usize) -> usize { - assert!( - alignment.is_power_of_two(), - "`alignment` must be a power of two" - ); - - let align_mask = alignment - 1; - if value & align_mask == 0 { - value // already aligned - } else { - (value | align_mask) + 1 - } -} - -/// Check if a value is aligned to a given alignment. -/// The alignment must be a power of 2. -#[inline(always)] -pub const fn is_aligned(value: usize, alignment: usize) -> bool { - assert!( - alignment.is_power_of_two(), - "`alignment` must be a power of two" - ); - - (value & (alignment - 1)) == 0 -} - -/// Convert a size into human readable format. -pub const fn size_human_readable_ceil(size: usize) -> (usize, &'static str) { - const KIB: usize = 1024; - const MIB: usize = 1024 * 1024; - const GIB: usize = 1024 * 1024 * 1024; - - if (size / GIB) > 0 { - (size.div_ceil(GIB), "GiB") - } else if (size / MIB) > 0 { - (size.div_ceil(MIB), "MiB") - } else if (size / KIB) > 0 { - (size.div_ceil(KIB), "KiB") - } else { - (size, "Byte") - } -} - -/// Calculate the next possible aligned address without sanity checking the -/// input parameters. -#[inline] -fn aligned_addr_unchecked(addr: usize, alignment: usize) -> usize { - (addr + (alignment - 1)) & !(alignment - 1) -} diff --git a/libs/memory/src/mmu/mapping_record.rs b/libs/memory/src/mmu/mapping_record.rs index cc180da36..1d3d342bb 100644 --- a/libs/memory/src/mmu/mapping_record.rs +++ b/libs/memory/src/mmu/mapping_record.rs @@ -9,7 +9,7 @@ use { Address, Physical, Virtual, types::{AccessPermissions, AttributeFields, MMIODescriptor, MemAttributes, MemoryRegion}, }, - crate::{mm, platform}, + crate::platform, liblocking::{self, InitStateLock}, liblog::{info, warn}, }; @@ -37,6 +37,7 @@ struct MappingRecord { // Global instances //-------------------------------------------------------------------------------------------------- +// FIXME: global state static KERNEL_MAPPING_RECORD: InitStateLock = InitStateLock::new(MappingRecord::new()); @@ -110,18 +111,23 @@ impl MappingRecord { .filter_map(|x| x.as_mut()) .filter(|x| x.attribute_fields.mem_attributes == MemAttributes::Device) .find(|x| { - if x.phys_start_addr != phys_region.start_addr() { - return false; - } - - if x.num_pages != phys_region.num_pages() { - return false; - } - - true + x.phys_start_addr == phys_region.start_addr() + && x.num_pages == phys_region.num_pages() }) } + /// Adds a new mapping to the mapping record. + /// + /// # Arguments + /// + /// * `name` - The name of the entity that owns the mapping. + /// * `virt_region` - The virtual memory region being mapped. + /// * `phys_region` - The physical memory region being mapped. + /// * `attr` - The memory attributes of the mapping. + /// + /// # Returns + /// + /// Returns `Ok(())` on success, or a string error message on failure. pub fn add( &mut self, name: &'static str, @@ -162,7 +168,7 @@ impl MappingRecord { let phys_start = i.phys_start_addr; let phys_end_inclusive = phys_start + (size - 1); - let (size, unit) = mm::size_human_readable_ceil(size); + let (size, unit) = crate::size_human_readable_ceil(size); let attr = match i.attribute_fields.mem_attributes { MemAttributes::CacheableDRAM => "C", diff --git a/libs/memory/src/mmu/mod.rs b/libs/memory/src/mmu/mod.rs index c9c13ff72..8110aa784 100644 --- a/libs/memory/src/mmu/mod.rs +++ b/libs/memory/src/mmu/mod.rs @@ -1,12 +1,29 @@ +/* + * SPDX-License-Identifier: BlueOak-1.0.0 + * Copyright (c) Berkus Decker + */ + +//! The arch-independent representation of a MMU and memory translation tables. + +// To test we need to impl this for x86_64, aarch64 and riscv64 arches +// and provide the same interface from the arch-independent layer. + +// Need to be able to +// a) create page table hierarchy at different granule size and addressing mode (user/kernel) +// b) inspect/walk table hierarchy and resolve virtual-to-physical addresses via provided tables +// c) modify/invalidate page hierarchy descriptors + use { - crate::{Address, Physical, Virtual, platform}, + crate::platform, core::num::NonZeroUsize, + libaddress::{Address, Physical, Virtual}, liblog::warn, snafu::Snafu, }; #[cfg(target_arch = "aarch64")] use crate::arch::aarch64::mmu as arch_mmu; +use crate::platform::memory::mmu::KernelGranule; mod mapping_record; pub mod page_alloc; @@ -190,7 +207,9 @@ pub unsafe fn kernel_map_mmio( mmio_descriptor: &MMIODescriptor, ) -> Result, &'static str> { let phys_region = MemoryRegion::from(*mmio_descriptor); - let offset_into_start_page = mmio_descriptor.start_addr().offset_into_page(); + let offset_into_start_page = mmio_descriptor + .start_addr() + .offset_into_page(&KernelGranule::SIZE); // FIXME: fixed page size // Check if an identical region has been mapped for another driver. If so, reuse it. let virt_addr = if let Some(addr) = diff --git a/libs/memory/src/mmu/page_alloc.rs b/libs/memory/src/mmu/page_alloc.rs index 4492cf7a0..1aa6991ca 100644 --- a/libs/memory/src/mmu/page_alloc.rs +++ b/libs/memory/src/mmu/page_alloc.rs @@ -6,8 +6,8 @@ use { super::MemoryRegion, - crate::{AddressType, Virtual}, core::num::NonZeroUsize, + libaddress::{AddressType, Virtual}, liblocking::IRQSafeNullLock, liblog::warn, }; @@ -17,7 +17,7 @@ use { //-------------------------------------------------------------------------------------------------- /// A page allocator that can be lazyily initialized. -pub struct PageAllocator { +pub struct PageAllocator { pool: Option>, } @@ -37,13 +37,13 @@ pub fn kernel_mmio_va_allocator() -> &'static IRQSafeNullLock Default for PageAllocator { +impl Default for PageAllocator { fn default() -> Self { Self::new() } } -impl PageAllocator { +impl PageAllocator { /// Create an instance. pub const fn new() -> Self { Self { pool: None } diff --git a/libs/memory/src/mmu/translation_table.rs b/libs/memory/src/mmu/translation_table.rs index dff6fb61b..2e6461850 100644 --- a/libs/memory/src/mmu/translation_table.rs +++ b/libs/memory/src/mmu/translation_table.rs @@ -5,7 +5,7 @@ use crate::arch::aarch64::mmu::translation_table as arch_translation_table; use { super::{AttributeFields, MemoryRegion}, - crate::{Address, Physical, Virtual}, + libaddress::{Address, Physical, Virtual}, }; //-------------------------------------------------------------------------------------------------- diff --git a/libs/memory/src/mmu/types.rs b/libs/memory/src/mmu/types.rs index d9d48516d..beafaa4d1 100644 --- a/libs/memory/src/mmu/types.rs +++ b/libs/memory/src/mmu/types.rs @@ -3,24 +3,25 @@ //-------------------------------------------------------------------------------------------------- use { - crate::{Address, AddressType, Physical, mm, platform::KernelGranule}, + crate::platform::KernelGranule, core::{ fmt::{self, Formatter}, iter::Step, num::NonZeroUsize, ops::Range, }, + libaddress::{Address, AddressType, Physical}, }; /// A wrapper type around [Address] that ensures page alignment. #[derive(Copy, Clone, Debug, Eq, PartialOrd, PartialEq)] -pub struct PageAddress { +pub struct PageAddress { inner: Address, } /// A type that describes a region of memory in quantities of pages. #[derive(Copy, Clone, Debug, Eq, PartialOrd, PartialEq)] -pub struct MemoryRegion { +pub struct MemoryRegion { start: PageAddress, end_exclusive: PageAddress, } @@ -70,7 +71,7 @@ pub struct MMIODescriptor { //------------------------------------------------------------------------------ // PageAddress //------------------------------------------------------------------------------ -impl PageAddress { +impl PageAddress { /// Unwraps the value. pub fn into_inner(self) -> Address { self.inner @@ -80,46 +81,49 @@ impl PageAddress { /// /// `count` is in units of [`PageAddress`]. For example, a count of 2 means `result = self + 2 * /// page_size`. - pub fn checked_offset(self, count: isize) -> Option { + pub fn checked_page_offset(self, count: isize) -> Option { if count == 0 { return Some(self); } - let delta = count.unsigned_abs().checked_mul(KernelGranule::SIZE)?; + let delta = count.unsigned_abs().checked_mul(KernelGranule::SIZE)? as u64; let result = if count.is_positive() { - self.inner.as_usize().checked_add(delta)? + self.inner.as_u64().checked_add(delta)? } else { - self.inner.as_usize().checked_sub(delta)? + self.inner.as_u64().checked_sub(delta)? }; Some(Self { - inner: Address::new(result), + inner: Address::::new(result), }) } } -impl From for PageAddress { +impl From for PageAddress { fn from(addr: usize) -> Self { assert!( - mm::is_aligned(addr, KernelGranule::SIZE), + libaddress::align::is_aligned(addr as u64, KernelGranule::SIZE as u64), "Input usize not page aligned" ); Self { - inner: Address::new(addr), + inner: Address::::new(addr as u64), } } } -impl From> for PageAddress { +impl From> for PageAddress { fn from(addr: Address) -> Self { - assert!(addr.is_page_aligned(), "Input Address not page aligned"); + assert!( + addr.is_page_aligned(&KernelGranule::SIZE), // FIXME: fixed page size + "Input Address not page aligned" + ); Self { inner: addr } } } -impl Step for PageAddress { +impl Step for PageAddress { fn steps_between(start: &Self, end: &Self) -> (usize, Option) { if start > end { return (0, None); @@ -131,18 +135,18 @@ impl Step for PageAddress { } fn forward_checked(start: Self, count: usize) -> Option { - start.checked_offset(count.cast_signed()) + start.checked_page_offset(count.cast_signed()) } fn backward_checked(start: Self, count: usize) -> Option { - start.checked_offset(-(count.cast_signed())) + start.checked_page_offset(-(count.cast_signed())) } } //------------------------------------------------------------------------------ // MemoryRegion //------------------------------------------------------------------------------ -impl MemoryRegion { +impl MemoryRegion { /// Create an instance. pub fn new(start: PageAddress, end_exclusive: PageAddress) -> Self { assert!(start <= end_exclusive); @@ -174,12 +178,12 @@ impl MemoryRegion { /// Returns the exclusive end page address. pub fn end_inclusive_page_addr(&self) -> PageAddress { - self.end_exclusive.checked_offset(-1).unwrap() + self.end_exclusive.checked_page_offset(-1).unwrap() } /// Checks if self contains an address. pub fn contains(&self, addr: Address) -> bool { - let page_addr = PageAddress::from(addr.align_down_page()); + let page_addr = PageAddress::from(addr.align_down_page(&KernelGranule::SIZE)); // FIXME: fixed page size self.as_range().contains(&page_addr) } @@ -205,7 +209,8 @@ impl MemoryRegion { end_exclusive - start } - /// Splits the `MemoryRegion` like: + /// Splits the MemoryRegion like in the following diagram. + /// Left region is returned to the caller. Right region is the new region for this struct. /// /// -------------------------------------------------------------------------------- /// | | | | | | | | | | | | | | | | | | | @@ -218,11 +223,10 @@ impl MemoryRegion { /// | | /// `right_start` `right_end_exclusive` /// - /// Left region is returned to the caller. Right region is the new region for this struct. pub fn take_first_n_pages(&mut self, num_pages: NonZeroUsize) -> Result { let count: usize = num_pages.into(); - let left_end_exclusive = self.start.checked_offset(count.cast_signed()); + let left_end_exclusive = self.start.checked_page_offset(count.cast_signed()); let Some(left_end_exclusive) = left_end_exclusive else { return Err("Overflow while calculating left_end_exclusive"); }; @@ -241,7 +245,7 @@ impl MemoryRegion { } } -impl IntoIterator for MemoryRegion { +impl IntoIterator for MemoryRegion { type Item = PageAddress; type IntoIter = Range; @@ -255,8 +259,11 @@ impl IntoIterator for MemoryRegion { impl From for MemoryRegion { fn from(desc: MMIODescriptor) -> Self { - let start = PageAddress::from(desc.start_addr.align_down_page()); - let end_exclusive = PageAddress::from(desc.end_addr_exclusive().align_up_page()); + let start = PageAddress::from(desc.start_addr.align_down_page(&KernelGranule::SIZE)); // FIXME: fixed page size + let end_exclusive = PageAddress::from( + desc.end_addr_exclusive() + .align_up_page(&KernelGranule::SIZE), // FIXME: fixed page size + ); Self { start, @@ -273,7 +280,7 @@ impl MMIODescriptor { /// Create an instance. pub const fn new(start_addr: Address, size: usize) -> Self { assert!(size > 0); - let end_addr_exclusive = Address::new(start_addr.as_usize() + size); + let end_addr_exclusive = Address::new(start_addr.as_u64() + size as u64); Self { start_addr, diff --git a/libs/memory/src/phys_addr.rs b/libs/memory/src/phys_addr.rs deleted file mode 100644 index 7484aa8da..000000000 --- a/libs/memory/src/phys_addr.rs +++ /dev/null @@ -1,254 +0,0 @@ -/* - * SPDX-License-Identifier: BlueOak-1.0.0 - * Copyright (c) Berkus Decker - */ - -use { - crate::{ - mm::{align_down, align_up}, - virt_addr::VirtAddr, - }, - bit_field::BitField, - core::{ - convert::From, - fmt, - ops::{Add, AddAssign, Shl, Shr, Sub, SubAssign}, - }, - usize_conversions::FromUsize, -}; - -/// A 64-bit physical memory address. -/// -/// This is a wrapper type around an `u64`, so it is always 8 bytes, even when compiled -/// on non 64-bit systems. The `UsizeConversions` trait can be used for performing conversions -/// between `u64` and `usize`. -/// -/// On `aarch64`, only the 52 lower bits of a physical address can be used. The top 12 bits need -/// to be zero. This type guarantees that it always represents a valid physical address. -#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Default)] -#[repr(transparent)] -pub struct PhysAddr(pub u64); - -/// A passed `u64` was not a valid physical address. -/// -/// This means that bits 52 to 64 were not all null. -#[derive(Debug, PartialEq, Eq, PartialOrd, Ord)] -pub struct PhysAddrNotValid(pub u64); - -impl PhysAddr { - /// Creates a new physical address. - /// - /// Panics if any bits in the bit position 52 to 64 is set. - pub fn new(addr: u64) -> PhysAddr { - assert_eq!( - addr.get_bits(52..64), - 0, - "physical addresses must not have any set bits in positions 52 to 64" - ); - PhysAddr(addr) - } - - /// Tries to create a new physical address. - /// - /// Fails if any bits in the bit positions 52 to 64 are set. - pub fn try_new(addr: u64) -> Result { - match addr.get_bits(52..64) { - 0 => Ok(PhysAddr(addr)), // address is valid - _ => Err(PhysAddrNotValid(addr)), - } - } - - /// Creates a physical address that points to `0`. - pub const fn zero() -> PhysAddr { - PhysAddr(0) - } - - /// Converts the address to an `u64`. - pub fn as_u64(self) -> u64 { - self.0 - } - - pub const fn as_ptr(self) -> *const T { - self.0 as *const T - } - - pub fn as_mut_ptr(self) -> *mut T { - self.0 as *mut T - } - - /// Convenience method for checking if a physical address is null. - pub fn is_null(&self) -> bool { - self.0 == 0 - } - - /// Aligns the physical address upwards to the given alignment. - /// - /// See the `align_up` function for more information. - #[must_use] - pub fn aligned_up(self, align: usize) -> Self { - PhysAddr( - align_up(self.0.try_into().unwrap(), align) - .try_into() - .unwrap(), - ) - } - - /// Aligns the physical address downwards to the given alignment. - /// - /// See the `align_down` function for more information. - #[must_use] - pub fn aligned_down(self, align: usize) -> Self { - PhysAddr( - align_down(self.0.try_into().unwrap(), align) - .try_into() - .unwrap(), - ) - } - - /// Checks whether the physical address has the demanded alignment. - pub fn is_aligned(self, align: usize) -> bool { - self.aligned_down(align) == self - } - - /// Convert physical memory address into a kernel-view virtual address for physical memory. - pub fn user_to_kernel(&self) -> VirtAddr { - use super::PHYSICAL_KERNEL_WINDOW; - assert!(self.0 < !PHYSICAL_KERNEL_WINDOW); // Can't have phys address over 1GiB then - VirtAddr::new(self.0 + PHYSICAL_KERNEL_WINDOW) - } -} - -impl fmt::Debug for PhysAddr { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "PhysAddr({:#x})", self.0) - } -} - -impl fmt::Binary for PhysAddr { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - self.0.fmt(f) - } -} - -impl fmt::LowerHex for PhysAddr { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - self.0.fmt(f) - } -} - -impl fmt::Octal for PhysAddr { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - self.0.fmt(f) - } -} - -impl fmt::UpperHex for PhysAddr { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - self.0.fmt(f) - } -} - -impl From for PhysAddr { - fn from(value: u64) -> Self { - PhysAddr::new(value) - } -} - -impl From for u64 { - fn from(value: PhysAddr) -> Self { - value.as_u64() - } -} - -impl From for u128 { - fn from(value: PhysAddr) -> Self { - u128::from(value.as_u64()) - } -} - -impl Add for PhysAddr { - type Output = Self; - fn add(self, rhs: u64) -> Self::Output { - PhysAddr::new(self.0 + rhs) - } -} - -impl AddAssign for PhysAddr { - fn add_assign(&mut self, rhs: u64) { - *self = *self + rhs; - } -} - -impl Add for PhysAddr -where - u64: FromUsize, -{ - type Output = Self; - fn add(self, rhs: usize) -> Self::Output { - self + u64::from_usize(rhs) - } -} - -impl AddAssign for PhysAddr -where - u64: FromUsize, -{ - fn add_assign(&mut self, rhs: usize) { - self.add_assign(u64::from_usize(rhs)); - } -} - -impl Sub for PhysAddr { - type Output = Self; - fn sub(self, rhs: u64) -> Self::Output { - PhysAddr::new(self.0.checked_sub(rhs).unwrap()) - } -} - -impl SubAssign for PhysAddr { - fn sub_assign(&mut self, rhs: u64) { - *self = *self - rhs; - } -} - -impl Sub for PhysAddr -where - u64: FromUsize, -{ - type Output = Self; - fn sub(self, rhs: usize) -> Self::Output { - self - u64::from_usize(rhs) - } -} - -impl SubAssign for PhysAddr -where - u64: FromUsize, -{ - fn sub_assign(&mut self, rhs: usize) { - self.sub_assign(u64::from_usize(rhs)); - } -} - -impl Sub for PhysAddr { - type Output = u64; - fn sub(self, rhs: PhysAddr) -> Self::Output { - self.as_u64().checked_sub(rhs.as_u64()).unwrap() - } -} - -impl Shr for PhysAddr { - type Output = PhysAddr; - - fn shr(self, shift: usize) -> Self::Output { - PhysAddr::new(self.0 >> shift) - } -} - -impl Shl for PhysAddr { - type Output = PhysAddr; - - fn shl(self, shift: usize) -> Self::Output { - PhysAddr::new(self.0 << shift) - } -} diff --git a/libs/memory/src/platform/raspberrypi/memory/mmu.rs b/libs/memory/src/platform/raspberrypi/memory/mmu.rs index 78dbb3175..1d41c99ca 100644 --- a/libs/memory/src/platform/raspberrypi/memory/mmu.rs +++ b/libs/memory/src/platform/raspberrypi/memory/mmu.rs @@ -1,12 +1,10 @@ //! Platform memory management unit. use { - crate::{ - Physical, Virtual, - mmu::{ - AddressSpace, AssociatedTranslationTable, MemoryRegion, PageAddress, TranslationGranule, - }, + crate::mmu::{ + AddressSpace, AssociatedTranslationTable, MemoryRegion, PageAddress, TranslationGranule, }, + libaddress::{Physical, Virtual}, liblocking::InitStateLock, }; @@ -59,7 +57,7 @@ pub fn virt_code_region() -> MemoryRegion { let start_page_addr = super::virt_code_start(); let end_exclusive_page_addr = start_page_addr - .checked_offset(num_pages.cast_signed()) + .checked_page_offset(num_pages.cast_signed()) .unwrap(); MemoryRegion::new(start_page_addr, end_exclusive_page_addr) @@ -71,7 +69,7 @@ pub fn virt_data_region() -> MemoryRegion { let start_page_addr = super::virt_data_start(); let end_exclusive_page_addr = start_page_addr - .checked_offset(num_pages.cast_signed()) + .checked_page_offset(num_pages.cast_signed()) .unwrap(); MemoryRegion::new(start_page_addr, end_exclusive_page_addr) @@ -83,7 +81,7 @@ pub fn virt_boot_core_stack_region() -> MemoryRegion { let start_page_addr = super::virt_boot_core_stack_start(); let end_exclusive_page_addr = start_page_addr - .checked_offset(num_pages.cast_signed()) + .checked_page_offset(num_pages.cast_signed()) .unwrap(); MemoryRegion::new(start_page_addr, end_exclusive_page_addr) @@ -179,7 +177,7 @@ pub fn virt_mmio_remap_region() -> MemoryRegion { let start_page_addr = super::virt_mmio_remap_start(); let end_exclusive_page_addr = start_page_addr - .checked_offset(num_pages.cast_signed()) + .checked_page_offset(num_pages.cast_signed()) .unwrap(); MemoryRegion::new(start_page_addr, end_exclusive_page_addr) diff --git a/libs/memory/src/platform/raspberrypi/memory/mod.rs b/libs/memory/src/platform/raspberrypi/memory/mod.rs index 77900c6f6..2ef4dfb85 100644 --- a/libs/memory/src/platform/raspberrypi/memory/mod.rs +++ b/libs/memory/src/platform/raspberrypi/memory/mod.rs @@ -63,8 +63,9 @@ pub mod mmu; //-------------------------------------------------------------------------------------------------- use { - crate::{Address, Physical, Virtual, mmu::PageAddress}, + crate::mmu::PageAddress, core::cell::UnsafeCell, + libaddress::{Address, Physical, Virtual}, }; // Symbols from the linker script. @@ -157,24 +158,24 @@ pub mod map { pub const MMIO_BASE: usize = 0x3F00_0000; /// Interrupt controller - pub const PERIPHERAL_IC_BASE: Address = Address::new(MMIO_BASE + 0x0000_B200); + pub const PERIPHERAL_IC_BASE: Address = Address::new((MMIO_BASE + 0x0000_B200) as u64); pub const PERIPHERAL_IC_SIZE: usize = 0x24; /// Base address of ARM<->VC mailbox area. - pub const VIDEOCORE_MBOX_BASE: Address = Address::new(MMIO_BASE + VIDEOCORE_MBOX_OFFSET); + pub const VIDEOCORE_MBOX_BASE: Address = Address::new((MMIO_BASE + VIDEOCORE_MBOX_OFFSET) as u64); /// Board power control. - pub const POWER_BASE: Address = Address::new(MMIO_BASE + POWER_OFFSET); + pub const POWER_BASE: Address = Address::new((MMIO_BASE + POWER_OFFSET) as u64); /// Base address of GPIO registers. - pub const GPIO_BASE: Address = Address::new(MMIO_BASE + GPIO_OFFSET); + pub const GPIO_BASE: Address = Address::new((MMIO_BASE + GPIO_OFFSET) as u64); pub const GPIO_SIZE: usize = 0xA0; - pub const PL011_UART_BASE: Address = Address::new(MMIO_BASE + UART_OFFSET); + pub const PL011_UART_BASE: Address = Address::new((MMIO_BASE + UART_OFFSET) as u64); pub const PL011_UART_SIZE: usize = 0x48; /// Base address of `MiniUART`. - pub const MINI_UART_BASE: Address = Address::new(MMIO_BASE + MINIUART_OFFSET); + pub const MINI_UART_BASE: Address = Address::new((MMIO_BASE + MINIUART_OFFSET) as u64); /// End of MMIO memory region. pub const END: Address = Address::new(0x4001_0000); diff --git a/libs/memory/src/virt_addr.rs b/libs/memory/src/virt_addr.rs deleted file mode 100644 index 5b02527a6..000000000 --- a/libs/memory/src/virt_addr.rs +++ /dev/null @@ -1,273 +0,0 @@ -/* - * SPDX-License-Identifier: BlueOak-1.0.0 - * Copyright (c) Berkus Decker - */ - -use { - crate::{ - mm::{align_down, align_up}, - phys_addr::PhysAddr, - }, - bit_field::BitField, - core::{ - convert::{From, TryInto}, - fmt, - ops::{Add, AddAssign, Rem, RemAssign, Sub, SubAssign}, - }, - usize_conversions::FromUsize, - ux::*, -}; - -/// A canonical 64-bit virtual memory address. -/// -/// This is a wrapper type around an `u64`, so it is always 8 bytes, even when compiled -/// on non 64-bit systems. The `UsizeConversions` trait can be used for performing conversions -/// between `u64` and `usize`. -/// -/// On `x86_64`, only the 48 lower bits of a virtual address can be used. The top 16 bits need -/// to be copies of bit 47, i.e. the most significant bit. Addresses that fulfil this criterium -/// are called “canonical”. This type guarantees that it always represents a canonical address. -#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Default)] -#[repr(transparent)] -pub struct VirtAddr(pub u64); - -/// A passed `u64` was not a valid virtual address. -/// -/// This means that bits 48 to 64 are not -/// a valid sign extension and are not null either. So automatic sign extension would have -/// overwritten possibly meaningful bits. This likely indicates a bug, for example an invalid -/// address calculation. -#[derive(Debug, PartialEq, Eq, PartialOrd, Ord)] -pub struct VirtAddrNotValid(u64); - -impl VirtAddr { - /// Creates a new canonical virtual address. - /// - /// This function performs sign extension of bit 47 to make the address canonical. Panics - /// if the bits in the range 48 to 64 contain data (i.e. are not null and not a sign extension). - /// - /// @todo Support ASID byte in top bits of the address. - pub fn new(addr: u64) -> VirtAddr { - Self::try_new(addr).expect( - "address passed to VirtAddr::new must not contain any data \ - in bits 48 to 64", - ) - } - - /// Tries to create a new canonical virtual address. - /// - /// This function tries to performs sign extension of bit 47 to make the address canonical. - /// It succeeds if bits 48 to 64 are either a correct sign extension (i.e. copies of bit 47) - /// or all null. Else, an error is returned. - pub fn try_new(addr: u64) -> Result { - match addr.get_bits(47..64) { - 0 | 0x1ffff => Ok(VirtAddr(addr)), // address is canonical - 1 => Ok(VirtAddr::new_unchecked(addr)), // address needs sign extension - _ => Err(VirtAddrNotValid(addr)), - } - } - - /// Creates a new canonical virtual address without checks. - /// - /// This function performs sign extension of bit 47 to make the address canonical, so - /// bits 48 to 64 are overwritten. If you want to check that these bits contain no data, - /// use `new` or `try_new`. - pub const fn new_unchecked(addr: u64) -> VirtAddr { - // FIXME: Constness! - // if addr.get_bit(47) { - // addr.set_bits(48..64, 0xffff); - // } else { - // addr.set_bits(48..64, 0); - // } - VirtAddr(addr) - } - - /// Creates a virtual address that points to `0`. - pub const fn zero() -> VirtAddr { - VirtAddr(0) - } - - /// Converts the address to an `u64`. - pub const fn as_u64(self) -> u64 { - self.0 - } - - /// Creates a virtual address from the given pointer - pub fn from_ptr(ptr: *const T) -> Self { - Self::new(u64::from_usize(ptr as usize)) - } - - /// Converts the address to a raw pointer. - #[cfg(target_pointer_width = "64")] - pub const fn as_ptr(self) -> *const T { - self.0 as *const T - } - - /// Converts the address to a mutable raw pointer. - #[cfg(target_pointer_width = "64")] - pub fn as_mut_ptr(self) -> *mut T { - self.as_ptr::().cast_mut() - } - - /// Aligns the virtual address upwards to the given alignment. - /// - /// See the `align_up` free function for more information. - #[must_use] - pub fn aligned_up(self, align: usize) -> Self { - VirtAddr( - align_up(self.0.try_into().unwrap(), align) - .try_into() - .unwrap(), - ) - } - - /// Aligns the virtual address downwards to the given alignment. - /// - /// See the `align_down` free function for more information. - #[must_use] - pub fn aligned_down(self, align: usize) -> Self { - VirtAddr( - align_down(self.0.try_into().unwrap(), align) - .try_into() - .unwrap(), - ) - } - - /// Checks whether the virtual address has the demanded alignment. - pub fn is_aligned(self, align: usize) -> bool { - self.aligned_down(align) == self - } - - /// Returns the 12-bit page offset of this virtual address. - pub fn page_offset(&self) -> u12 { - u12::new((self.0 & 0xfff).try_into().unwrap()) - } - // ^ @todo this only works for 4KiB pages - - /// Returns the 9-bit level 3 page table index. - pub fn l3_index(&self) -> u9 { - u9::new(((self.0 >> 12) & 0o777).try_into().unwrap()) - } - - /// Returns the 9-bit level 2 page table index. - pub fn l2_index(&self) -> u9 { - u9::new(((self.0 >> 12 >> 9) & 0o777).try_into().unwrap()) - } - - /// Returns the 9-bit level 1 page table index. - pub fn l1_index(&self) -> u9 { - u9::new(((self.0 >> 12 >> 9 >> 9) & 0o777).try_into().unwrap()) - } - - /// Returns the 9-bit level 0 page table index. - pub fn l0_index(&self) -> u9 { - u9::new(((self.0 >> 12 >> 9 >> 9 >> 9) & 0o777).try_into().unwrap()) - } - - pub const fn is_higher_half(self) -> bool { - self.0 >= 0xFFFF_0000_0000_0000 - } - - /// Convert kernel-view virtual address of physical memory into a physical memory address. - pub fn kernel_to_user(&self) -> PhysAddr { - use super::PHYSICAL_KERNEL_WINDOW; - assert!(self.0 > PHYSICAL_KERNEL_WINDOW); - PhysAddr::new(self.0 - PHYSICAL_KERNEL_WINDOW) - } -} - -impl fmt::Debug for VirtAddr { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "VirtAddr({:#x})", self.0) - } -} - -impl fmt::Binary for VirtAddr { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - self.0.fmt(f) - } -} - -impl fmt::LowerHex for VirtAddr { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - self.0.fmt(f) - } -} - -impl fmt::Octal for VirtAddr { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - self.0.fmt(f) - } -} - -impl fmt::UpperHex for VirtAddr { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - self.0.fmt(f) - } -} - -impl From for VirtAddr { - fn from(value: u64) -> Self { - VirtAddr::new(value) - } -} - -impl From for u64 { - fn from(value: VirtAddr) -> Self { - value.as_u64() - } -} - -impl Add for VirtAddr { - type Output = Self; - /// Add a given offset to the current virtual address. Never wraps. - fn add(self, rhs: T) -> Self::Output { - // @todo runtime cost of unwrap() here - VirtAddr::new(self.0.saturating_add(num::cast(rhs).unwrap())) - } -} - -impl AddAssign for VirtAddr { - fn add_assign(&mut self, rhs: T) { - *self = *self + rhs; - } -} - -impl Sub for VirtAddr { - type Output = Self; - /// Subtract a given offset from the current virtual address. Never wraps. - fn sub(self, rhs: T) -> Self::Output { - // @todo runtime cost of unwrap() here - VirtAddr::new(self.0.saturating_sub(num::cast(rhs).unwrap())) - } -} - -impl SubAssign for VirtAddr { - fn sub_assign(&mut self, rhs: T) { - *self = *self - rhs; - } -} - -impl Sub for VirtAddr { - type Output = u64; - /// Produce a difference between two virtual addresses. - fn sub(self, rhs: VirtAddr) -> Self::Output { - self.as_u64().checked_sub(rhs.as_u64()).unwrap() // @todo use i64? - } -} - -impl Rem for VirtAddr { - type Output = u64; - fn rem(self, rhs: T) -> Self::Output { - num::traits::CheckedRem::checked_rem(&self.0, &num::cast(rhs).unwrap()).unwrap() - } -} - -// @todo this is not very useful... -impl RemAssign for VirtAddr { - fn rem_assign(&mut self, rhs: T) { - *self = VirtAddr::new( - num::traits::CheckedRem::checked_rem(&self.0, &num::cast(rhs).unwrap()).unwrap(), - ); - } -} diff --git a/libs/object/Cargo.toml b/libs/object/Cargo.toml index 5eb9ebd04..d9bbbb501 100644 --- a/libs/object/Cargo.toml +++ b/libs/object/Cargo.toml @@ -19,6 +19,7 @@ publish = false maintenance = { status = "experimental" } [dependencies] +libaddress = { workspace = true } libmemory = { workspace = true } libprint = { workspace = true } libqemu = { workspace = true } diff --git a/libs/object/src/domain.rs b/libs/object/src/domain.rs index 3ece9c39c..d8241afdb 100644 --- a/libs/object/src/domain.rs +++ b/libs/object/src/domain.rs @@ -1,7 +1,7 @@ use { crate::{CapError, Key, KeySlot}, core::sync::atomic::{AtomicU32, AtomicU64, Ordering}, - libmemory::virt_addr::VirtAddr, + libaddress::VirtAddr, libsyscall::{protected_call0, protected_call2}, }; @@ -393,7 +393,7 @@ impl DcbView { /// Must only be called after kernel has set up the mapping pub const unsafe fn from_user_mapping() -> Self { // FIXME: Duplicate DcbPages::USER_BASE const from nucleus/objects/domain.rs here, keep in sync! - const USER_BASE: VirtAddr = VirtAddr::new_unchecked(0x0000_7FFF_FE00_0000); + const USER_BASE: VirtAddr = unsafe { VirtAddr::new_unchecked(0x0000_7FFF_FE00_0000) }; Self { base: USER_BASE.as_ptr() as *const DomainControlBlock, } diff --git a/libs/platform/Cargo.toml b/libs/platform/Cargo.toml index 30d35c482..577591681 100644 --- a/libs/platform/Cargo.toml +++ b/libs/platform/Cargo.toml @@ -32,6 +32,7 @@ bit_field = { workspace = true } bitflags = { workspace = true } buddy-alloc = { workspace = true } cfg-if = { workspace = true } +libaddress = { workspace = true } libconsole = { workspace = true } libcpu = { workspace = true } libexception = { workspace = true } diff --git a/libs/platform/README.md b/libs/platform/README.md new file mode 100644 index 000000000..39247568b --- /dev/null +++ b/libs/platform/README.md @@ -0,0 +1,7 @@ +# Board Support Packages + +This directory contains support for specific Boards like RaspberryPi3 etc. + +---- + +For more information please re-read. diff --git a/libs/platform/src/platform/raspberrypi/device_driver/bcm/gpio.rs b/libs/platform/src/platform/raspberrypi/device_driver/bcm/gpio.rs index 51400ada5..087c23264 100644 --- a/libs/platform/src/platform/raspberrypi/device_driver/bcm/gpio.rs +++ b/libs/platform/src/platform/raspberrypi/device_driver/bcm/gpio.rs @@ -8,8 +8,8 @@ use { crate::platform::device_driver::{IRQNumber, common::MMIODerefWrapper}, core::marker::PhantomData, + libaddress::{Address, Virtual}, liblocking::{IRQSafeNullLock, interface::Mutex}, - libmemory::{Address, Virtual}, tock_registers::{ fields::FieldValue, interfaces::{ReadWriteable, Readable, Writeable}, diff --git a/libs/platform/src/platform/raspberrypi/device_driver/bcm/interrupt_controller/mod.rs b/libs/platform/src/platform/raspberrypi/device_driver/bcm/interrupt_controller/mod.rs index 571eca55d..faf5554ab 100644 --- a/libs/platform/src/platform/raspberrypi/device_driver/bcm/interrupt_controller/mod.rs +++ b/libs/platform/src/platform/raspberrypi/device_driver/bcm/interrupt_controller/mod.rs @@ -8,8 +8,8 @@ mod peripheral_ic; use { core::fmt, + libaddress::{Address, Virtual}, libexception::asynchronous::{IRQHandlerDescriptor, interface::IRQManager}, - libmemory::{Address, Virtual}, libprimitives::BoundedUsize, }; diff --git a/libs/platform/src/platform/raspberrypi/device_driver/bcm/interrupt_controller/peripheral_ic.rs b/libs/platform/src/platform/raspberrypi/device_driver/bcm/interrupt_controller/peripheral_ic.rs index bf70b3d52..d429b6855 100644 --- a/libs/platform/src/platform/raspberrypi/device_driver/bcm/interrupt_controller/peripheral_ic.rs +++ b/libs/platform/src/platform/raspberrypi/device_driver/bcm/interrupt_controller/peripheral_ic.rs @@ -99,8 +99,8 @@ impl PeripheralIC { // OS Interface Code //------------------------------------------------------------------------------ use { + libaddress::{Address, Virtual}, liblocking::interface::{Mutex, ReadWriteEx}, - libmemory::{Address, Virtual}, }; impl IRQManager for PeripheralIC { diff --git a/libs/platform/src/platform/raspberrypi/device_driver/bcm/mailbox.rs b/libs/platform/src/platform/raspberrypi/device_driver/bcm/mailbox.rs index debe4fabe..9fba9d22b 100644 --- a/libs/platform/src/platform/raspberrypi/device_driver/bcm/mailbox.rs +++ b/libs/platform/src/platform/raspberrypi/device_driver/bcm/mailbox.rs @@ -384,7 +384,7 @@ impl Mailbox< /// # Safety /// Caller is responsible for picking the correct MMIO register base address. pub unsafe fn new(base_addr: usize) -> Result> { - let base = libmemory::Address::new(base_addr); + let base = libaddress::Address::new(base_addr as u64); Ok(Mailbox { registers: Registers::new(base), buffer: Storage::new()?, diff --git a/libs/platform/src/platform/raspberrypi/device_driver/bcm/mini_uart.rs b/libs/platform/src/platform/raspberrypi/device_driver/bcm/mini_uart.rs index 5ecfd58e4..332b415a5 100644 --- a/libs/platform/src/platform/raspberrypi/device_driver/bcm/mini_uart.rs +++ b/libs/platform/src/platform/raspberrypi/device_driver/bcm/mini_uart.rs @@ -17,9 +17,9 @@ use { convert::From, fmt::{self, Arguments}, }, + libaddress::{Address, Virtual}, libconsole::{SerialOps, console::interface}, liblocking::{IRQSafeNullLock, interface::Mutex}, - libmemory::{Address, Virtual}, tock_registers::{ interfaces::ReadWriteable, register_bitfields, register_structs, diff --git a/libs/platform/src/platform/raspberrypi/device_driver/bcm/pl011_uart.rs b/libs/platform/src/platform/raspberrypi/device_driver/bcm/pl011_uart.rs index 8341df6a6..f1330cb74 100644 --- a/libs/platform/src/platform/raspberrypi/device_driver/bcm/pl011_uart.rs +++ b/libs/platform/src/platform/raspberrypi/device_driver/bcm/pl011_uart.rs @@ -11,9 +11,9 @@ use { crate::platform::device_driver::{IRQNumber, common::MMIODerefWrapper, gpio}, core::fmt::{self, Arguments}, + libaddress::{Address, Virtual}, libconsole::{SerialOps, console::interface}, liblocking::{IRQSafeNullLock, interface::Mutex}, - libmemory::{Address, Virtual}, libprimitives::cpu::loop_while, // snafu::Snafu, tock_registers::{ diff --git a/libs/platform/src/platform/raspberrypi/device_driver/bcm/power.rs b/libs/platform/src/platform/raspberrypi/device_driver/bcm/power.rs index 9d3bb17a2..2f6d5e823 100644 --- a/libs/platform/src/platform/raspberrypi/device_driver/bcm/power.rs +++ b/libs/platform/src/platform/raspberrypi/device_driver/bcm/power.rs @@ -11,7 +11,7 @@ use { gpio, mailbox::{Mailbox, MailboxOps, channel}, }, - libmemory::{Address, Virtual}, + libaddress::{Address, Virtual}, snafu::Snafu, tock_registers::{ interfaces::{Readable, Writeable}, diff --git a/libs/platform/src/platform/raspberrypi/device_driver/common.rs b/libs/platform/src/platform/raspberrypi/device_driver/common.rs index 0d1bbe2fa..1f72e776a 100644 --- a/libs/platform/src/platform/raspberrypi/device_driver/common.rs +++ b/libs/platform/src/platform/raspberrypi/device_driver/common.rs @@ -7,7 +7,7 @@ use { core::{marker::PhantomData, ops}, - libmemory::{Address, Virtual}, + libaddress::{Address, Virtual}, }; //-------------------------------------------------------------------------------------------------- diff --git a/libs/platform/src/platform/raspberrypi/fb.rs b/libs/platform/src/platform/raspberrypi/fb.rs index 031f1b899..d933949f9 100644 --- a/libs/platform/src/platform/raspberrypi/fb.rs +++ b/libs/platform/src/platform/raspberrypi/fb.rs @@ -2,7 +2,7 @@ use { crate::platform::raspberrypi::device_driver::bcm::mailbox::{ self, LocalMailboxStorage, Mailbox, MailboxError, MailboxOps, }, - libmemory::{Address, Virtual}, + libaddress::{Address, Virtual}, }; /// `FrameBuffer` channel supported structure - use with `mailbox::channel::FrameBuffer` From b52cd2d9e82ea750d909df7c1e0617dab34fd118 Mon Sep 17 00:00:00 2001 From: Berkus Decker Date: Sun, 8 Feb 2026 14:48:53 +0200 Subject: [PATCH 067/107] wip: extract liballoc --- libs/address/README.md | 13 +++++++++ libs/alloc/Cargo.toml | 29 +++++++++++++++++++ .../src/mm => alloc/src}/bump_allocator.rs | 0 .../src/mm/mod.rs => alloc/src/lib.rs} | 0 libs/memory/src/arch/aarch64/addr/asid.rs | 7 ----- libs/memory/src/arch/aarch64/addr/docs.md | 12 -------- libs/memory/src/arch/aarch64/addr/mod.rs | 7 ----- .../memory/src/arch/aarch64/boot_allocator.rs | 15 ---------- libs/memory/src/arch/aarch64/mod.rs | 1 - libs/memory/src/lib.rs | 1 - 10 files changed, 42 insertions(+), 43 deletions(-) create mode 100644 libs/alloc/Cargo.toml rename libs/{memory/src/mm => alloc/src}/bump_allocator.rs (100%) rename libs/{memory/src/mm/mod.rs => alloc/src/lib.rs} (100%) delete mode 100644 libs/memory/src/arch/aarch64/addr/asid.rs delete mode 100644 libs/memory/src/arch/aarch64/addr/docs.md delete mode 100644 libs/memory/src/arch/aarch64/addr/mod.rs delete mode 100644 libs/memory/src/arch/aarch64/boot_allocator.rs diff --git a/libs/address/README.md b/libs/address/README.md index abe6fe26c..6cecaea86 100644 --- a/libs/address/README.md +++ b/libs/address/README.md @@ -2,6 +2,19 @@ The types Address and Address represent the addresses before and after the mapping in the MMU. +## Arch-independent + +Address is a 64-bit unsigned number (_address_ of a location in memory) + +It can be aligned and checked for alignment. +There are not other limitations on the address. + +## Arch-dependent + +Address may have a size limitation, e.g. a 40-bits physical address on some platforms. - could be platform-dependent, not arch-dependent! + +Address may contain additional information payload, for example ASID tags. + ## Exports This library should export the following: diff --git a/libs/alloc/Cargo.toml b/libs/alloc/Cargo.toml new file mode 100644 index 000000000..6290306f8 --- /dev/null +++ b/libs/alloc/Cargo.toml @@ -0,0 +1,29 @@ +[package] +name = "liballoc" + +description = "Vesper nanokernel memory allocator types." + +authors = { workspace = true } +categories = { workspace = true } +documentation = { workspace = true } +edition = { workspace = true } +homepage = { workspace = true } +license = { workspace = true } +readme = { workspace = true } +repository = { workspace = true } +version = { workspace = true } + +publish = false + +[badges] +maintenance = { status = "experimental" } + +[dependencies] +libaddress = { workspace = true } +liblog = { workspace = true } + +[lib] +test = false + +[lints] +workspace = true diff --git a/libs/memory/src/mm/bump_allocator.rs b/libs/alloc/src/bump_allocator.rs similarity index 100% rename from libs/memory/src/mm/bump_allocator.rs rename to libs/alloc/src/bump_allocator.rs diff --git a/libs/memory/src/mm/mod.rs b/libs/alloc/src/lib.rs similarity index 100% rename from libs/memory/src/mm/mod.rs rename to libs/alloc/src/lib.rs diff --git a/libs/memory/src/arch/aarch64/addr/asid.rs b/libs/memory/src/arch/aarch64/addr/asid.rs deleted file mode 100644 index 5855abcfd..000000000 --- a/libs/memory/src/arch/aarch64/addr/asid.rs +++ /dev/null @@ -1,7 +0,0 @@ -/* - * SPDX-License-Identifier: BlueOak-1.0.0 - * Copyright (c) Berkus Decker - */ - -#[allow(dead_code)] -pub type Asid = u16; diff --git a/libs/memory/src/arch/aarch64/addr/docs.md b/libs/memory/src/arch/aarch64/addr/docs.md deleted file mode 100644 index e4e11a05b..000000000 --- a/libs/memory/src/arch/aarch64/addr/docs.md +++ /dev/null @@ -1,12 +0,0 @@ -## Arch-independent - -Address is a 64-bit unsigned number (_address_ of a location in memory) - -It can be aligned and checked for alignment. -There are not other limitations on the address. - -## Arch-dependent - -Address may have a size limitation, e.g. a 40-bits physical address on some platforms. - could be platform-dependent, not arch-dependent! - -Address may contain additional information payload, for example ASID tags. diff --git a/libs/memory/src/arch/aarch64/addr/mod.rs b/libs/memory/src/arch/aarch64/addr/mod.rs deleted file mode 100644 index 9a5d580f2..000000000 --- a/libs/memory/src/arch/aarch64/addr/mod.rs +++ /dev/null @@ -1,7 +0,0 @@ -/* - * SPDX-License-Identifier: BlueOak-1.0.0 - * Copyright (c) Berkus Decker - */ - -pub mod asid; -// pub use asid::*; diff --git a/libs/memory/src/arch/aarch64/boot_allocator.rs b/libs/memory/src/arch/aarch64/boot_allocator.rs deleted file mode 100644 index 14b5e6712..000000000 --- a/libs/memory/src/arch/aarch64/boot_allocator.rs +++ /dev/null @@ -1,15 +0,0 @@ -/* - * SPDX-License-Identifier: BlueOak-1.0.0 - */ -// Allocate regions from boot memory list obtained from devtree -pub struct BootRegionAllocator {} - -impl BootRegionAllocator { - pub fn new(&boot_info: BootInfo) -> Self { - Self {} - } - - pub fn alloc_region(&mut self) {} - - pub fn alloc_zeroed(&mut self) {} -} diff --git a/libs/memory/src/arch/aarch64/mod.rs b/libs/memory/src/arch/aarch64/mod.rs index f8a99ef6f..8c0e53ce7 100644 --- a/libs/memory/src/arch/aarch64/mod.rs +++ b/libs/memory/src/arch/aarch64/mod.rs @@ -5,7 +5,6 @@ //! Memory management functions for aarch64. -mod addr; pub mod features; // @todo make only pub re-export? pub mod mmu; mod page_size; diff --git a/libs/memory/src/lib.rs b/libs/memory/src/lib.rs index d46f6bbac..c87ecc4b4 100644 --- a/libs/memory/src/lib.rs +++ b/libs/memory/src/lib.rs @@ -17,7 +17,6 @@ #![feature(custom_test_frameworks)] mod arch; -pub mod mm; pub mod mmu; pub mod platform; From 9ad736d119b61c15ecd644d14c65cd0e354c68b5 Mon Sep 17 00:00:00 2001 From: Berkus Decker Date: Sun, 8 Feb 2026 15:14:33 +0200 Subject: [PATCH 068/107] =?UTF-8?q?refactor:=20=E2=99=BB=EF=B8=8F=20Flatte?= =?UTF-8?q?n=20libplatform?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- libs/platform/README.md | 2 +- libs/platform/src/lib.rs | 11 ++++++++- libs/platform/src/platform/README.md | 7 ------ libs/platform/src/platform/mod.rs | 10 -------- .../src/{platform => }/raspberrypi/cpu.rs | 0 .../device_driver/arm/gicv2/gicc.rs | 0 .../device_driver/arm/gicv2/gicd.rs | 0 .../device_driver/arm/gicv2/mod.rs | 0 .../raspberrypi/device_driver/arm/mod.rs | 0 .../raspberrypi/device_driver/bcm/gpio.rs | 2 +- .../bcm/interrupt_controller/mod.rs | 0 .../bcm/interrupt_controller/peripheral_ic.rs | 2 +- .../raspberrypi/device_driver/bcm/mailbox.rs | 2 +- .../device_driver/bcm/mailbox_refactor.rs | 0 .../device_driver/bcm/mini_uart.rs | 2 +- .../raspberrypi/device_driver/bcm/mod.rs | 0 .../device_driver/bcm/pl011_uart.rs | 4 ++-- .../raspberrypi/device_driver/bcm/power.rs | 2 +- .../raspberrypi/device_driver/common.rs | 0 .../raspberrypi/device_driver/mod.rs | 0 .../src/{platform => }/raspberrypi/display.rs | 0 .../src/{platform => }/raspberrypi/drivers.rs | 4 ++-- .../raspberrypi/exception/asynchronous.rs | 10 ++++---- .../raspberrypi/exception/mod.rs | 0 .../src/{platform => }/raspberrypi/fb.rs | 2 +- .../raspberrypi/linker/init_thread.ld | 0 .../raspberrypi/linker/nucleus.ld | 0 .../src/{platform => }/raspberrypi/mod.rs | 0 .../src/{platform => }/raspberrypi/vc.rs | 24 +++++++++---------- 29 files changed, 38 insertions(+), 46 deletions(-) delete mode 100644 libs/platform/src/platform/README.md delete mode 100644 libs/platform/src/platform/mod.rs rename libs/platform/src/{platform => }/raspberrypi/cpu.rs (100%) rename libs/platform/src/{platform => }/raspberrypi/device_driver/arm/gicv2/gicc.rs (100%) rename libs/platform/src/{platform => }/raspberrypi/device_driver/arm/gicv2/gicd.rs (100%) rename libs/platform/src/{platform => }/raspberrypi/device_driver/arm/gicv2/mod.rs (100%) rename libs/platform/src/{platform => }/raspberrypi/device_driver/arm/mod.rs (100%) rename libs/platform/src/{platform => }/raspberrypi/device_driver/bcm/gpio.rs (99%) rename libs/platform/src/{platform => }/raspberrypi/device_driver/bcm/interrupt_controller/mod.rs (100%) rename libs/platform/src/{platform => }/raspberrypi/device_driver/bcm/interrupt_controller/peripheral_ic.rs (98%) rename libs/platform/src/{platform => }/raspberrypi/device_driver/bcm/mailbox.rs (99%) rename libs/platform/src/{platform => }/raspberrypi/device_driver/bcm/mailbox_refactor.rs (100%) rename libs/platform/src/{platform => }/raspberrypi/device_driver/bcm/mini_uart.rs (99%) rename libs/platform/src/{platform => }/raspberrypi/device_driver/bcm/mod.rs (100%) rename libs/platform/src/{platform => }/raspberrypi/device_driver/bcm/pl011_uart.rs (99%) rename libs/platform/src/{platform => }/raspberrypi/device_driver/bcm/power.rs (98%) rename libs/platform/src/{platform => }/raspberrypi/device_driver/common.rs (100%) rename libs/platform/src/{platform => }/raspberrypi/device_driver/mod.rs (100%) rename libs/platform/src/{platform => }/raspberrypi/display.rs (100%) rename libs/platform/src/{platform => }/raspberrypi/drivers.rs (98%) rename libs/platform/src/{platform => }/raspberrypi/exception/asynchronous.rs (90%) rename libs/platform/src/{platform => }/raspberrypi/exception/mod.rs (100%) rename libs/platform/src/{platform => }/raspberrypi/fb.rs (97%) rename libs/platform/src/{platform => }/raspberrypi/linker/init_thread.ld (100%) rename libs/platform/src/{platform => }/raspberrypi/linker/nucleus.ld (100%) rename libs/platform/src/{platform => }/raspberrypi/mod.rs (100%) rename libs/platform/src/{platform => }/raspberrypi/vc.rs (85%) diff --git a/libs/platform/README.md b/libs/platform/README.md index 39247568b..04a966c95 100644 --- a/libs/platform/README.md +++ b/libs/platform/README.md @@ -1,6 +1,6 @@ # Board Support Packages -This directory contains support for specific Boards like RaspberryPi3 etc. +This library contains support for specific Boards like RaspberryPi3 etc. ---- diff --git a/libs/platform/src/lib.rs b/libs/platform/src/lib.rs index e6e57f94f..98852d8a6 100644 --- a/libs/platform/src/lib.rs +++ b/libs/platform/src/lib.rs @@ -1,3 +1,8 @@ +/* + * SPDX-License-Identifier: BlueOak-1.0.0 + * Copyright (c) Berkus Decker + */ + #![no_std] #![no_main] #![feature(decl_macro)] @@ -7,4 +12,8 @@ #![test_runner(libtest::test_runner)] #![reexport_test_harness_main = "test_main"] -pub mod platform; // @todo flatten! +#[cfg(any(board_rpi3, board_rpi4))] +pub mod raspberrypi; + +#[cfg(any(board_rpi3, board_rpi4))] +pub use raspberrypi::*; diff --git a/libs/platform/src/platform/README.md b/libs/platform/src/platform/README.md deleted file mode 100644 index 39247568b..000000000 --- a/libs/platform/src/platform/README.md +++ /dev/null @@ -1,7 +0,0 @@ -# Board Support Packages - -This directory contains support for specific Boards like RaspberryPi3 etc. - ----- - -For more information please re-read. diff --git a/libs/platform/src/platform/mod.rs b/libs/platform/src/platform/mod.rs deleted file mode 100644 index 5d0aacb8f..000000000 --- a/libs/platform/src/platform/mod.rs +++ /dev/null @@ -1,10 +0,0 @@ -/* - * SPDX-License-Identifier: BlueOak-1.0.0 - * Copyright (c) Berkus Decker - */ - -#[cfg(any(board_rpi3, board_rpi4))] -pub mod raspberrypi; - -#[cfg(any(board_rpi3, board_rpi4))] -pub use raspberrypi::*; diff --git a/libs/platform/src/platform/raspberrypi/cpu.rs b/libs/platform/src/raspberrypi/cpu.rs similarity index 100% rename from libs/platform/src/platform/raspberrypi/cpu.rs rename to libs/platform/src/raspberrypi/cpu.rs diff --git a/libs/platform/src/platform/raspberrypi/device_driver/arm/gicv2/gicc.rs b/libs/platform/src/raspberrypi/device_driver/arm/gicv2/gicc.rs similarity index 100% rename from libs/platform/src/platform/raspberrypi/device_driver/arm/gicv2/gicc.rs rename to libs/platform/src/raspberrypi/device_driver/arm/gicv2/gicc.rs diff --git a/libs/platform/src/platform/raspberrypi/device_driver/arm/gicv2/gicd.rs b/libs/platform/src/raspberrypi/device_driver/arm/gicv2/gicd.rs similarity index 100% rename from libs/platform/src/platform/raspberrypi/device_driver/arm/gicv2/gicd.rs rename to libs/platform/src/raspberrypi/device_driver/arm/gicv2/gicd.rs diff --git a/libs/platform/src/platform/raspberrypi/device_driver/arm/gicv2/mod.rs b/libs/platform/src/raspberrypi/device_driver/arm/gicv2/mod.rs similarity index 100% rename from libs/platform/src/platform/raspberrypi/device_driver/arm/gicv2/mod.rs rename to libs/platform/src/raspberrypi/device_driver/arm/gicv2/mod.rs diff --git a/libs/platform/src/platform/raspberrypi/device_driver/arm/mod.rs b/libs/platform/src/raspberrypi/device_driver/arm/mod.rs similarity index 100% rename from libs/platform/src/platform/raspberrypi/device_driver/arm/mod.rs rename to libs/platform/src/raspberrypi/device_driver/arm/mod.rs diff --git a/libs/platform/src/platform/raspberrypi/device_driver/bcm/gpio.rs b/libs/platform/src/raspberrypi/device_driver/bcm/gpio.rs similarity index 99% rename from libs/platform/src/platform/raspberrypi/device_driver/bcm/gpio.rs rename to libs/platform/src/raspberrypi/device_driver/bcm/gpio.rs index 087c23264..951085e50 100644 --- a/libs/platform/src/platform/raspberrypi/device_driver/bcm/gpio.rs +++ b/libs/platform/src/raspberrypi/device_driver/bcm/gpio.rs @@ -6,7 +6,7 @@ */ use { - crate::platform::device_driver::{IRQNumber, common::MMIODerefWrapper}, + crate::device_driver::{IRQNumber, common::MMIODerefWrapper}, core::marker::PhantomData, libaddress::{Address, Virtual}, liblocking::{IRQSafeNullLock, interface::Mutex}, diff --git a/libs/platform/src/platform/raspberrypi/device_driver/bcm/interrupt_controller/mod.rs b/libs/platform/src/raspberrypi/device_driver/bcm/interrupt_controller/mod.rs similarity index 100% rename from libs/platform/src/platform/raspberrypi/device_driver/bcm/interrupt_controller/mod.rs rename to libs/platform/src/raspberrypi/device_driver/bcm/interrupt_controller/mod.rs diff --git a/libs/platform/src/platform/raspberrypi/device_driver/bcm/interrupt_controller/peripheral_ic.rs b/libs/platform/src/raspberrypi/device_driver/bcm/interrupt_controller/peripheral_ic.rs similarity index 98% rename from libs/platform/src/platform/raspberrypi/device_driver/bcm/interrupt_controller/peripheral_ic.rs rename to libs/platform/src/raspberrypi/device_driver/bcm/interrupt_controller/peripheral_ic.rs index d429b6855..c78a00542 100644 --- a/libs/platform/src/platform/raspberrypi/device_driver/bcm/interrupt_controller/peripheral_ic.rs +++ b/libs/platform/src/raspberrypi/device_driver/bcm/interrupt_controller/peripheral_ic.rs @@ -10,7 +10,7 @@ use { super::{PendingIRQs, PeripheralIRQ}, - crate::platform::device_driver::common::MMIODerefWrapper, + crate::device_driver::common::MMIODerefWrapper, libexception::asynchronous::{IRQContext, IRQHandlerDescriptor, interface::IRQManager}, liblocking::{self, IRQSafeNullLock, InitStateLock}, tock_registers::{ diff --git a/libs/platform/src/platform/raspberrypi/device_driver/bcm/mailbox.rs b/libs/platform/src/raspberrypi/device_driver/bcm/mailbox.rs similarity index 99% rename from libs/platform/src/platform/raspberrypi/device_driver/bcm/mailbox.rs rename to libs/platform/src/raspberrypi/device_driver/bcm/mailbox.rs index 9fba9d22b..9c8a0485a 100644 --- a/libs/platform/src/platform/raspberrypi/device_driver/bcm/mailbox.rs +++ b/libs/platform/src/raspberrypi/device_driver/bcm/mailbox.rs @@ -12,7 +12,7 @@ #![allow(dead_code)] use { - crate::platform::{BcmHost, device_driver::common::MMIODerefWrapper}, + crate::{BcmHost, device_driver::common::MMIODerefWrapper}, aarch64_cpu::asm::barrier, core::{ // alloc::{AllocError, Allocator, Layout}, diff --git a/libs/platform/src/platform/raspberrypi/device_driver/bcm/mailbox_refactor.rs b/libs/platform/src/raspberrypi/device_driver/bcm/mailbox_refactor.rs similarity index 100% rename from libs/platform/src/platform/raspberrypi/device_driver/bcm/mailbox_refactor.rs rename to libs/platform/src/raspberrypi/device_driver/bcm/mailbox_refactor.rs diff --git a/libs/platform/src/platform/raspberrypi/device_driver/bcm/mini_uart.rs b/libs/platform/src/raspberrypi/device_driver/bcm/mini_uart.rs similarity index 99% rename from libs/platform/src/platform/raspberrypi/device_driver/bcm/mini_uart.rs rename to libs/platform/src/raspberrypi/device_driver/bcm/mini_uart.rs index 332b415a5..cceaf3003 100644 --- a/libs/platform/src/platform/raspberrypi/device_driver/bcm/mini_uart.rs +++ b/libs/platform/src/raspberrypi/device_driver/bcm/mini_uart.rs @@ -8,7 +8,7 @@ // #[cfg(not(feature = "noserial"))] // use tock_registers::interfaces::{Readable, Writeable}; use { - crate::platform::{ + crate::{ BcmHost, device_driver::{common::MMIODerefWrapper, gpio}, exception::asynchronous::IRQNumber, diff --git a/libs/platform/src/platform/raspberrypi/device_driver/bcm/mod.rs b/libs/platform/src/raspberrypi/device_driver/bcm/mod.rs similarity index 100% rename from libs/platform/src/platform/raspberrypi/device_driver/bcm/mod.rs rename to libs/platform/src/raspberrypi/device_driver/bcm/mod.rs diff --git a/libs/platform/src/platform/raspberrypi/device_driver/bcm/pl011_uart.rs b/libs/platform/src/raspberrypi/device_driver/bcm/pl011_uart.rs similarity index 99% rename from libs/platform/src/platform/raspberrypi/device_driver/bcm/pl011_uart.rs rename to libs/platform/src/raspberrypi/device_driver/bcm/pl011_uart.rs index f1330cb74..e97a0c334 100644 --- a/libs/platform/src/platform/raspberrypi/device_driver/bcm/pl011_uart.rs +++ b/libs/platform/src/raspberrypi/device_driver/bcm/pl011_uart.rs @@ -9,7 +9,7 @@ */ use { - crate::platform::device_driver::{IRQNumber, common::MMIODerefWrapper, gpio}, + crate::device_driver::{IRQNumber, common::MMIODerefWrapper, gpio}, core::fmt::{self, Arguments}, libaddress::{Address, Virtual}, libconsole::{SerialOps, console::interface}, @@ -520,7 +520,7 @@ impl libdriver::drivers::interface::DeviceDriver for PL011Uart { irq_number: Self::IRQNumberType, ) -> Result<(), &'static str> { use { - crate::platform::exception::asynchronous::irq_manager, + crate::exception::asynchronous::irq_manager, libexception::asynchronous::IRQHandlerDescriptor, }; diff --git a/libs/platform/src/platform/raspberrypi/device_driver/bcm/power.rs b/libs/platform/src/raspberrypi/device_driver/bcm/power.rs similarity index 98% rename from libs/platform/src/platform/raspberrypi/device_driver/bcm/power.rs rename to libs/platform/src/raspberrypi/device_driver/bcm/power.rs index 2f6d5e823..84be2bb6e 100644 --- a/libs/platform/src/platform/raspberrypi/device_driver/bcm/power.rs +++ b/libs/platform/src/raspberrypi/device_driver/bcm/power.rs @@ -6,7 +6,7 @@ */ use { - crate::platform::device_driver::{ + crate::device_driver::{ common::MMIODerefWrapper, gpio, mailbox::{Mailbox, MailboxOps, channel}, diff --git a/libs/platform/src/platform/raspberrypi/device_driver/common.rs b/libs/platform/src/raspberrypi/device_driver/common.rs similarity index 100% rename from libs/platform/src/platform/raspberrypi/device_driver/common.rs rename to libs/platform/src/raspberrypi/device_driver/common.rs diff --git a/libs/platform/src/platform/raspberrypi/device_driver/mod.rs b/libs/platform/src/raspberrypi/device_driver/mod.rs similarity index 100% rename from libs/platform/src/platform/raspberrypi/device_driver/mod.rs rename to libs/platform/src/raspberrypi/device_driver/mod.rs diff --git a/libs/platform/src/platform/raspberrypi/display.rs b/libs/platform/src/raspberrypi/display.rs similarity index 100% rename from libs/platform/src/platform/raspberrypi/display.rs rename to libs/platform/src/raspberrypi/display.rs diff --git a/libs/platform/src/platform/raspberrypi/drivers.rs b/libs/platform/src/raspberrypi/drivers.rs similarity index 98% rename from libs/platform/src/platform/raspberrypi/drivers.rs rename to libs/platform/src/raspberrypi/drivers.rs index 5efb93f25..129386571 100644 --- a/libs/platform/src/platform/raspberrypi/drivers.rs +++ b/libs/platform/src/raspberrypi/drivers.rs @@ -1,6 +1,6 @@ use { super::exception, // @todo - crate::platform::{device_driver, exception::asynchronous::IRQNumber}, + crate::{device_driver, exception::asynchronous::IRQNumber}, core::{ mem::MaybeUninit, sync::atomic::{AtomicBool, Ordering}, @@ -197,7 +197,7 @@ unsafe fn instantiate_interrupt_controller() -> Result<(), &'static str> { #[allow(clippy::unnecessary_wraps)] unsafe fn post_init_interrupt_controller() -> Result<(), &'static str> { #[allow(static_mut_refs)] - crate::platform::exception::asynchronous::register_irq_manager( + crate::exception::asynchronous::register_irq_manager( // SAFETY: You may believe praying will save you. unsafe { INTERRUPT_CONTROLLER.assume_init_ref() }, ); diff --git a/libs/platform/src/platform/raspberrypi/exception/asynchronous.rs b/libs/platform/src/raspberrypi/exception/asynchronous.rs similarity index 90% rename from libs/platform/src/platform/raspberrypi/exception/asynchronous.rs rename to libs/platform/src/raspberrypi/exception/asynchronous.rs index 24e795a28..1b269a2d2 100644 --- a/libs/platform/src/platform/raspberrypi/exception/asynchronous.rs +++ b/libs/platform/src/raspberrypi/exception/asynchronous.rs @@ -14,18 +14,18 @@ use { //-------------------------------------------------------------------------------------------------- /// Export for reuse in generic asynchronous.rs. -pub use crate::platform::device_driver::IRQNumber; // @todo +pub use crate::device_driver::IRQNumber; // @todo #[cfg(board_rpi3)] -pub(in crate::platform) mod irq_map { - use crate::platform::device_driver::{IRQNumber, PeripheralIRQ}; +pub(crate) mod irq_map { + use crate::device_driver::{IRQNumber, PeripheralIRQ}; pub const PL011_UART: IRQNumber = IRQNumber::Peripheral(PeripheralIRQ::new(57)); } #[cfg(board_rpi4)] -pub(in crate::platform) mod irq_map { - use crate::platform::device_driver::IRQNumber; +pub(crate) mod irq_map { + use crate::device_driver::IRQNumber; pub const PL011_UART: IRQNumber = IRQNumber::new(153); } diff --git a/libs/platform/src/platform/raspberrypi/exception/mod.rs b/libs/platform/src/raspberrypi/exception/mod.rs similarity index 100% rename from libs/platform/src/platform/raspberrypi/exception/mod.rs rename to libs/platform/src/raspberrypi/exception/mod.rs diff --git a/libs/platform/src/platform/raspberrypi/fb.rs b/libs/platform/src/raspberrypi/fb.rs similarity index 97% rename from libs/platform/src/platform/raspberrypi/fb.rs rename to libs/platform/src/raspberrypi/fb.rs index d933949f9..04c6d012a 100644 --- a/libs/platform/src/platform/raspberrypi/fb.rs +++ b/libs/platform/src/raspberrypi/fb.rs @@ -1,5 +1,5 @@ use { - crate::platform::raspberrypi::device_driver::bcm::mailbox::{ + crate::raspberrypi::device_driver::bcm::mailbox::{ self, LocalMailboxStorage, Mailbox, MailboxError, MailboxOps, }, libaddress::{Address, Virtual}, diff --git a/libs/platform/src/platform/raspberrypi/linker/init_thread.ld b/libs/platform/src/raspberrypi/linker/init_thread.ld similarity index 100% rename from libs/platform/src/platform/raspberrypi/linker/init_thread.ld rename to libs/platform/src/raspberrypi/linker/init_thread.ld diff --git a/libs/platform/src/platform/raspberrypi/linker/nucleus.ld b/libs/platform/src/raspberrypi/linker/nucleus.ld similarity index 100% rename from libs/platform/src/platform/raspberrypi/linker/nucleus.ld rename to libs/platform/src/raspberrypi/linker/nucleus.ld diff --git a/libs/platform/src/platform/raspberrypi/mod.rs b/libs/platform/src/raspberrypi/mod.rs similarity index 100% rename from libs/platform/src/platform/raspberrypi/mod.rs rename to libs/platform/src/raspberrypi/mod.rs diff --git a/libs/platform/src/platform/raspberrypi/vc.rs b/libs/platform/src/raspberrypi/vc.rs similarity index 85% rename from libs/platform/src/platform/raspberrypi/vc.rs rename to libs/platform/src/raspberrypi/vc.rs index d75442716..9560b7a6f 100644 --- a/libs/platform/src/platform/raspberrypi/vc.rs +++ b/libs/platform/src/raspberrypi/vc.rs @@ -3,7 +3,7 @@ * Copyright (c) Berkus Decker */ use { - crate::platform::raspberrypi::{ + crate::raspberrypi::{ BcmHost, device_driver::bcm::mailbox::{ self, Mailbox, MailboxOps, MailboxStorageRef, channel, response::VAL_LEN_FLAG, @@ -32,17 +32,17 @@ impl VC { // Use framebuffer mailbox interface to initialize // https://www.raspberrypi.org/forums/viewtopic.php?f=72&t=185116 pub fn init_fb(w: u32, h: u32, depth: u32) -> Result { - /* - * * All tags in the request are processed in one operation. - * * It is not valid to mix Test tags with Get/Set tags - * in the same operation and no tags will be returned. - * * Get tags will be processed after all Set tags. - * * If an allocate buffer tag is omitted when setting parameters, - * then no change occurs unless it can be accommodated without changing - * the buffer base or size. - * * When an allocate buffer response is returned, the old buffer area - * (if the base or size has changed) is implicitly freed. - */ + // + // - All tags in the request are processed in one operation. + // - It is not valid to mix Test tags with Get/Set tags + // in the same operation and no tags will be returned. + // - Get tags will be processed after all Set tags. + // - If an allocate buffer tag is omitted when setting parameters, + // then no change occurs unless it can be accommodated without changing + // the buffer base or size. + // - When an allocate buffer response is returned, the old buffer area + // (if the base or size has changed) is implicitly freed. + // // control: MailboxCommand<10, FrameBufferData> let mut mbox = Mailbox::<36>::default(); From 6b8bdbd765dd1f67bdca3ac8e27d32206ac18043 Mon Sep 17 00:00:00 2001 From: Berkus Decker Date: Sun, 8 Feb 2026 15:03:20 +0200 Subject: [PATCH 069/107] =?UTF-8?q?refactor:=20=E2=99=BB=EF=B8=8F=20Flatte?= =?UTF-8?q?n=20and=20clean=20up=20libmemory?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/arch/aarch64/mmu/translation_table.rs | 23 +- libs/memory/src/lib.rs | 290 +++++++++++++- libs/memory/src/{mmu => }/mapping_record.rs | 0 libs/memory/src/mmu/mod.rs | 291 -------------- libs/memory/src/mmu/unused.rs | 124 ------ libs/memory/src/{mmu => }/page_alloc.rs | 0 libs/memory/src/platform/README.md | 1 - libs/memory/src/platform/mod.rs | 4 - .../src/platform/raspberrypi/memory/mmu.rs | 288 -------------- .../src/platform/raspberrypi/memory/mod.rs | 370 ------------------ libs/memory/src/platform/raspberrypi/mod.rs | 1 - .../memory/src/{mmu => }/translation_table.rs | 0 libs/memory/src/{mmu => }/types.rs | 0 libs/platform/src/raspberrypi/memory.rs | 146 +++++++ libs/platform/src/raspberrypi/mod.rs | 1 + 15 files changed, 440 insertions(+), 1099 deletions(-) rename libs/memory/src/{mmu => }/mapping_record.rs (100%) delete mode 100644 libs/memory/src/mmu/mod.rs delete mode 100644 libs/memory/src/mmu/unused.rs rename libs/memory/src/{mmu => }/page_alloc.rs (100%) delete mode 100644 libs/memory/src/platform/README.md delete mode 100644 libs/memory/src/platform/mod.rs delete mode 100644 libs/memory/src/platform/raspberrypi/memory/mmu.rs delete mode 100644 libs/memory/src/platform/raspberrypi/memory/mod.rs delete mode 100644 libs/memory/src/platform/raspberrypi/mod.rs rename libs/memory/src/{mmu => }/translation_table.rs (100%) rename libs/memory/src/{mmu => }/types.rs (100%) create mode 100644 libs/platform/src/raspberrypi/memory.rs diff --git a/libs/memory/src/arch/aarch64/mmu/translation_table.rs b/libs/memory/src/arch/aarch64/mmu/translation_table.rs index ef685eb93..ce1d709d3 100644 --- a/libs/memory/src/arch/aarch64/mmu/translation_table.rs +++ b/libs/memory/src/arch/aarch64/mmu/translation_table.rs @@ -5,12 +5,9 @@ use core::{ use { super::{Granule64KiB, Granule512MiB, mair}, - crate::{ - mmu::{AccessPermissions, AttributeFields, MemAttributes, MemoryRegion, PageAddress}, - platform, - }, + crate::{AccessPermissions, AttributeFields, MemAttributes, MemoryRegion, PageAddress}, core::convert, - libaddress::{Address, Physical, Virtual}, + libaddress::{Address, PhysAddr, Physical, Virtual}, tock_registers::{ interfaces::{Readable, Writeable}, register_bitfields, @@ -120,7 +117,7 @@ pub struct PageDescriptor { } pub trait BaseAddr { - fn phys_start_addr(&self) -> Address; + fn phys_start_addr(&self) -> PhysAddr; fn base_addr_u64(&self) -> u64; fn base_addr_usize(&self) -> usize; } @@ -152,7 +149,7 @@ pub struct FixedSizeTranslationTable { impl BaseAddr for [T; N] { // The binary is still identity mapped, so we don't need to convert here. - fn phys_start_addr(&self) -> Address { + fn phys_start_addr(&self) -> PhysAddr { Address::from_ptr(core::ptr::from_ref(self)) } @@ -174,7 +171,7 @@ impl TableDescriptor { } /// Create an instance pointing to the supplied address. - pub fn from_next_lvl_table_addr(phys_next_lvl_table_addr: Address) -> Self { + pub fn from_next_lvl_table_addr(phys_next_lvl_table_addr: PhysAddr) -> Self { let val = InMemoryRegister::::new(0); let shifted = phys_next_lvl_table_addr.as_usize() >> Granule64KiB::SHIFT; @@ -266,7 +263,7 @@ impl convert::From //-------------------------------------------------------------------------------------------------- // Public Code //-------------------------------------------------------------------------------------------------- -use crate::mmu::{AddressSpace, AssociatedTranslationTable}; +use crate::{AddressSpace, AssociatedTranslationTable}; impl AssociatedTranslationTable for AddressSpace where @@ -285,7 +282,7 @@ impl FixedSizeTranslationTable { /// Create an instance. #[allow(clippy::assertions_on_constants)] pub const fn new() -> Self { - assert!(platform::memory::mmu::KernelGranule::SIZE == Granule64KiB::SIZE); // assert! is const-fn-friendly + assert!(libplatform::memory::KernelGranule::SIZE == Granule64KiB::SIZE); // assert! is const-fn-friendly // Can't have a zero-sized address space. assert!(NUM_TABLES > 0); @@ -412,9 +409,7 @@ impl FixedSizeTranslationTable { } } -impl crate::mmu::translation_table::interface::TranslationTable - for FixedSizeTranslationTable -{ +impl crate::TranslationTable for FixedSizeTranslationTable { fn init(&mut self) -> Result<(), &'static str> { if self.initialized { return Ok(()); @@ -436,7 +431,7 @@ impl crate::mmu::translation_table::interface::Translat Ok(()) } - fn phys_base_address(&self) -> Address { + fn phys_base_address(&self) -> PhysAddr { self.lvl2.phys_start_addr() } diff --git a/libs/memory/src/lib.rs b/libs/memory/src/lib.rs index c87ecc4b4..b0df57fad 100644 --- a/libs/memory/src/lib.rs +++ b/libs/memory/src/lib.rs @@ -1,8 +1,17 @@ -// SPDX-License-Identifier: MIT OR Apache-2.0 -// -// Copyright (c) 2018-2022 Andre Richter +/* + * SPDX-License-Identifier: BlueOak-1.0.0 + * Copyright (c) Berkus Decker + */ -//! Memory Management. +//! The arch-independent representation of a MMU and memory translation tables. + +// To test we need to impl this for x86_64, aarch64 and riscv64 arches +// and provide the same interface from the arch-independent layer. + +// Need to be able to +// a) create page table hierarchy at different granule size and addressing mode (user/kernel) +// b) inspect/walk table hierarchy and resolve virtual-to-physical addresses via provided tables +// c) modify/invalidate page hierarchy descriptors #![no_std] #![allow(dead_code)] // while refactoring @@ -16,9 +25,27 @@ #![feature(step_trait)] #![feature(custom_test_frameworks)] +use { + core::num::NonZeroUsize, + interface::MMU, + libaddress::{Address, Physical, Virtual}, + liblocking::interface::*, + liblog::warn, + snafu::Snafu, + translation_table::interface::TranslationTable, +}; + +#[cfg(target_arch = "aarch64")] +use crate::arch::aarch64::mmu as arch_mmu; +use crate::platform::memory::KernelGranule; + mod arch; -pub mod mmu; -pub mod platform; +mod mapping_record; +pub mod page_alloc; +pub mod translation_table; +mod types; + +pub use types::*; /// Convert a size into human readable format. pub const fn size_human_readable_ceil(size: usize) -> (usize, &'static str) { @@ -36,3 +63,254 @@ pub const fn size_human_readable_ceil(size: usize) -> (usize, &'static str) { (size, "Byte") } } + +//-------------------------------------------------------------------------------------------------- +// Architectural Public Reexports +//-------------------------------------------------------------------------------------------------- +// pub use arch_mmu::mmu; + +//-------------------------------------------------------------------------------------------------- +// Public Definitions +//-------------------------------------------------------------------------------------------------- + +/// MMU enable errors variants. +// @todo rework error types +#[allow(missing_docs)] +#[derive(Debug, Snafu)] +pub enum MMUEnableError { + #[snafu(display("MMU is already enabled"))] + AlreadyEnabled, + #[snafu(display("{}", err))] + Other { err: &'static str }, +} + +/// Memory Management interfaces. +pub mod interface { + use super::*; + + /// MMU functions. + pub trait MMU { + /// Turns on the MMU for the first time and enables data and instruction caching. + /// + /// # Safety + /// + /// - Changes the hardware's global state. + unsafe fn enable_mmu_and_caching( + &self, + phys_tables_base_addr: Address, + ) -> Result<(), MMUEnableError>; + + /// Returns true if the MMU is enabled, false otherwise. + fn is_enabled(&self) -> bool; + + fn print_features(&self); // debug + } +} + +/// Describes the characteristics of a translation granule. +pub struct TranslationGranule; + +/// Describes properties of an address space. +pub struct AddressSpace; + +/// Intended to be implemented for [`AddressSpace`]. +pub trait AssociatedTranslationTable { + /// A translation table whose address range is: + /// + /// [`AS_SIZE` - 1, 0] + type TableStartFromBottom; +} + +//-------------------------------------------------------------------------------------------------- +// Private Code +//-------------------------------------------------------------------------------------------------- + +// Query the platform for the reserved virtual addresses for MMIO remapping +// and initialize the kernel's MMIO VA allocator with it. +// fn kernel_init_mmio_va_allocator() { +// let region = platform::memory::mmu::virt_mmio_remap_region(); +// page_alloc::kernel_mmio_va_allocator().lock(|allocator| allocator.init(region)); +// } + +/// Map a region in the kernel's translation tables. +/// +/// No input checks done, input is passed through to the architectural implementation. +/// +/// # Safety +/// +/// - See `map_at()`. +/// - Does not prevent aliasing. +unsafe fn kernel_map_at_unchecked( + name: &'static str, + virt_region: &MemoryRegion, + phys_region: &MemoryRegion, + attr: AttributeFields, +) -> Result<(), &'static str> { + crate::platform::memory::mmu::kernel_translation_tables().write(|tables| + // SAFETY: Make a mistake and you're dead, gaijin! + unsafe { tables.map_at(virt_region, phys_region, attr) })?; + + if let Err(x) = mapping_record::kernel_add(name, virt_region, phys_region, attr) { + warn!("{x}"); + } + + Ok(()) +} + +//-------------------------------------------------------------------------------------------------- +// Public Code +//-------------------------------------------------------------------------------------------------- + +impl TranslationGranule { + /// The granule's size. + pub const SIZE: usize = Self::size_checked(); + + /// The granule's mask. + pub const MASK: usize = Self::SIZE - 1; + + /// The granule's shift, aka log2(size). + pub const SHIFT: usize = Self::SIZE.trailing_zeros() as usize; + + const fn size_checked() -> usize { + assert!(GRANULE_SIZE.is_power_of_two()); + + GRANULE_SIZE + } +} + +impl AddressSpace { + /// The address space size. + pub const SIZE: usize = Self::size_checked(); + + /// The address space shift, aka log2(size). + pub const SIZE_SHIFT: usize = Self::SIZE.trailing_zeros() as usize; + + const fn size_checked() -> usize { + assert!(AS_SIZE.is_power_of_two()); + + // Check for architectural restrictions as well. + Self::arch_address_space_size_sanity_checks(); + + AS_SIZE + } +} + +//-------------------------------------------------------------------------------------------------- +// Public Code +// FIXME: this code should move to init_thread and be only used during boot-up! (see paging.rs) +//-------------------------------------------------------------------------------------------------- + +/// Raw mapping of a virtual to physical region in the kernel translation tables. +/// +/// Prevents mapping into the MMIO range of the tables. +/// +/// # Safety +/// +/// - See `kernel_map_at_unchecked()`. +/// - Does not prevent aliasing. Currently, the callers must be trusted. +pub unsafe fn kernel_map_at( + name: &'static str, + virt_region: &MemoryRegion, + phys_region: &MemoryRegion, + attr: AttributeFields, +) -> Result<(), &'static str> { + if platform::memory::mmu::virt_mmio_remap_region().overlaps(virt_region) { + return Err("Attempt to manually map into MMIO region"); + } + + // SAFETY: Make a mistake and you're dead, gaijin! + unsafe { + kernel_map_at_unchecked(name, virt_region, phys_region, attr)?; + } + + Ok(()) +} + +/// MMIO remapping in the kernel translation tables. +/// +/// Typically used by device drivers. +/// +/// # Safety +/// +/// - Same as `kernel_map_at_unchecked()`, minus the aliasing part. +pub unsafe fn kernel_map_mmio( + name: &'static str, + mmio_descriptor: &MMIODescriptor, +) -> Result, &'static str> { + let phys_region = MemoryRegion::from(*mmio_descriptor); + let offset_into_start_page = mmio_descriptor + .start_addr() + .offset_into_page(&KernelGranule::SIZE); // FIXME: fixed page size + + // Check if an identical region has been mapped for another driver. If so, reuse it. + let virt_addr = if let Some(addr) = + mapping_record::kernel_find_and_insert_mmio_duplicate(mmio_descriptor, name) + { + addr + // Otherwise, allocate a new region and map it. + } else { + let Some(num_pages) = NonZeroUsize::new(phys_region.num_pages()) else { + return Err("Requested 0 pages"); + }; + + let virt_region = + page_alloc::kernel_mmio_va_allocator().lock(|allocator| allocator.alloc(num_pages))?; + + // SAFETY: Make a mistake and you're dead, gaijin! + unsafe { + kernel_map_at_unchecked( + name, + &virt_region, + &phys_region, + AttributeFields { + mem_attributes: MemAttributes::Device, + acc_perms: AccessPermissions::ReadWrite, + execute_never: true, + }, + )?; + } + + virt_region.start_addr() + }; + + Ok(virt_addr + offset_into_start_page) +} + +/// Map the kernel's binary. Returns the translation table's base address. +/// +/// # Safety +/// +/// - See [`bsp::memory::mmu::kernel_map_binary()`]. +pub unsafe fn kernel_map_binary() -> Result, &'static str> { + let phys_kernel_tables_base_addr = + platform::memory::mmu::kernel_translation_tables().write(|tables| { + tables.init().unwrap(); + tables.phys_base_address() + }); + + // SAFETY: Make a mistake and you're dead, gaijin! + unsafe { + platform::memory::mmu::kernel_map_binary()?; + } + + Ok(phys_kernel_tables_base_addr) +} + +/// Enable the MMU and data + instruction caching. +/// +/// # Safety +/// +/// - Crucial function during kernel init. Changes the the complete memory view of the processor. +#[inline] +pub unsafe fn enable_mmu_and_caching( + phys_tables_base_addr: Address, +) -> Result<(), MMUEnableError> { + // SAFETY: Make a mistake and you're dead, gaijin! + unsafe { arch_mmu::mmu().enable_mmu_and_caching(phys_tables_base_addr) } +} + +/// Human-readable print of all recorded kernel mappings. +#[inline] +pub fn kernel_print_mappings() { + mapping_record::kernel_print(); +} diff --git a/libs/memory/src/mmu/mapping_record.rs b/libs/memory/src/mapping_record.rs similarity index 100% rename from libs/memory/src/mmu/mapping_record.rs rename to libs/memory/src/mapping_record.rs diff --git a/libs/memory/src/mmu/mod.rs b/libs/memory/src/mmu/mod.rs deleted file mode 100644 index 8110aa784..000000000 --- a/libs/memory/src/mmu/mod.rs +++ /dev/null @@ -1,291 +0,0 @@ -/* - * SPDX-License-Identifier: BlueOak-1.0.0 - * Copyright (c) Berkus Decker - */ - -//! The arch-independent representation of a MMU and memory translation tables. - -// To test we need to impl this for x86_64, aarch64 and riscv64 arches -// and provide the same interface from the arch-independent layer. - -// Need to be able to -// a) create page table hierarchy at different granule size and addressing mode (user/kernel) -// b) inspect/walk table hierarchy and resolve virtual-to-physical addresses via provided tables -// c) modify/invalidate page hierarchy descriptors - -use { - crate::platform, - core::num::NonZeroUsize, - libaddress::{Address, Physical, Virtual}, - liblog::warn, - snafu::Snafu, -}; - -#[cfg(target_arch = "aarch64")] -use crate::arch::aarch64::mmu as arch_mmu; -use crate::platform::memory::mmu::KernelGranule; - -mod mapping_record; -pub mod page_alloc; -pub mod translation_table; -mod types; - -pub use types::*; - -//-------------------------------------------------------------------------------------------------- -// Architectural Public Reexports -//-------------------------------------------------------------------------------------------------- -// pub use arch_mmu::mmu; - -//-------------------------------------------------------------------------------------------------- -// Public Definitions -//-------------------------------------------------------------------------------------------------- - -/// MMU enable errors variants. -// @todo rework error types -#[allow(missing_docs)] -#[derive(Debug, Snafu)] -pub enum MMUEnableError { - #[snafu(display("MMU is already enabled"))] - AlreadyEnabled, - #[snafu(display("{}", err))] - Other { err: &'static str }, -} - -/// Memory Management interfaces. -pub mod interface { - use super::*; - - /// MMU functions. - pub trait MMU { - /// Turns on the MMU for the first time and enables data and instruction caching. - /// - /// # Safety - /// - /// - Changes the hardware's global state. - unsafe fn enable_mmu_and_caching( - &self, - phys_tables_base_addr: Address, - ) -> Result<(), MMUEnableError>; - - /// Returns true if the MMU is enabled, false otherwise. - fn is_enabled(&self) -> bool; - - fn print_features(&self); // debug - } -} - -/// Describes the characteristics of a translation granule. -pub struct TranslationGranule; - -/// Describes properties of an address space. -pub struct AddressSpace; - -/// Intended to be implemented for [`AddressSpace`]. -pub trait AssociatedTranslationTable { - /// A translation table whose address range is: - /// - /// [`AS_SIZE` - 1, 0] - type TableStartFromBottom; -} - -//-------------------------------------------------------------------------------------------------- -// Private Code -//-------------------------------------------------------------------------------------------------- -use {interface::MMU, liblocking::interface::*, translation_table::interface::TranslationTable}; - -/// Query the platform for the reserved virtual addresses for MMIO remapping -/// and initialize the kernel's MMIO VA allocator with it. -fn kernel_init_mmio_va_allocator() { - let region = platform::memory::mmu::virt_mmio_remap_region(); - - page_alloc::kernel_mmio_va_allocator().lock(|allocator| allocator.init(region)); -} - -/// Map a region in the kernel's translation tables. -/// -/// No input checks done, input is passed through to the architectural implementation. -/// -/// # Safety -/// -/// - See `map_at()`. -/// - Does not prevent aliasing. -unsafe fn kernel_map_at_unchecked( - name: &'static str, - virt_region: &MemoryRegion, - phys_region: &MemoryRegion, - attr: AttributeFields, -) -> Result<(), &'static str> { - crate::platform::memory::mmu::kernel_translation_tables().write(|tables| - // SAFETY: Make a mistake and you're dead, gaijin! - unsafe { tables.map_at(virt_region, phys_region, attr) })?; - - if let Err(x) = mapping_record::kernel_add(name, virt_region, phys_region, attr) { - warn!("{x}"); - } - - Ok(()) -} - -//-------------------------------------------------------------------------------------------------- -// Public Code -//-------------------------------------------------------------------------------------------------- - -impl TranslationGranule { - /// The granule's size. - pub const SIZE: usize = Self::size_checked(); - - /// The granule's mask. - pub const MASK: usize = Self::SIZE - 1; - - /// The granule's shift, aka log2(size). - pub const SHIFT: usize = Self::SIZE.trailing_zeros() as usize; - - const fn size_checked() -> usize { - assert!(GRANULE_SIZE.is_power_of_two()); - - GRANULE_SIZE - } -} - -impl AddressSpace { - /// The address space size. - pub const SIZE: usize = Self::size_checked(); - - /// The address space shift, aka log2(size). - pub const SIZE_SHIFT: usize = Self::SIZE.trailing_zeros() as usize; - - const fn size_checked() -> usize { - assert!(AS_SIZE.is_power_of_two()); - - // Check for architectural restrictions as well. - Self::arch_address_space_size_sanity_checks(); - - AS_SIZE - } -} - -//-------------------------------------------------------------------------------------------------- -// Public Code -//-------------------------------------------------------------------------------------------------- - -/// Raw mapping of a virtual to physical region in the kernel translation tables. -/// -/// Prevents mapping into the MMIO range of the tables. -/// -/// # Safety -/// -/// - See `kernel_map_at_unchecked()`. -/// - Does not prevent aliasing. Currently, the callers must be trusted. -pub unsafe fn kernel_map_at( - name: &'static str, - virt_region: &MemoryRegion, - phys_region: &MemoryRegion, - attr: AttributeFields, -) -> Result<(), &'static str> { - if platform::memory::mmu::virt_mmio_remap_region().overlaps(virt_region) { - return Err("Attempt to manually map into MMIO region"); - } - - // SAFETY: Make a mistake and you're dead, gaijin! - unsafe { - kernel_map_at_unchecked(name, virt_region, phys_region, attr)?; - } - - Ok(()) -} - -/// MMIO remapping in the kernel translation tables. -/// -/// Typically used by device drivers. -/// -/// # Safety -/// -/// - Same as `kernel_map_at_unchecked()`, minus the aliasing part. -pub unsafe fn kernel_map_mmio( - name: &'static str, - mmio_descriptor: &MMIODescriptor, -) -> Result, &'static str> { - let phys_region = MemoryRegion::from(*mmio_descriptor); - let offset_into_start_page = mmio_descriptor - .start_addr() - .offset_into_page(&KernelGranule::SIZE); // FIXME: fixed page size - - // Check if an identical region has been mapped for another driver. If so, reuse it. - let virt_addr = if let Some(addr) = - mapping_record::kernel_find_and_insert_mmio_duplicate(mmio_descriptor, name) - { - addr - // Otherwise, allocate a new region and map it. - } else { - let Some(num_pages) = NonZeroUsize::new(phys_region.num_pages()) else { - return Err("Requested 0 pages"); - }; - - let virt_region = - page_alloc::kernel_mmio_va_allocator().lock(|allocator| allocator.alloc(num_pages))?; - - // SAFETY: Make a mistake and you're dead, gaijin! - unsafe { - kernel_map_at_unchecked( - name, - &virt_region, - &phys_region, - AttributeFields { - mem_attributes: MemAttributes::Device, - acc_perms: AccessPermissions::ReadWrite, - execute_never: true, - }, - )?; - } - - virt_region.start_addr() - }; - - Ok(virt_addr + offset_into_start_page) -} - -/// Map the kernel's binary. Returns the translation table's base address. -/// -/// # Safety -/// -/// - See [`bsp::memory::mmu::kernel_map_binary()`]. -pub unsafe fn kernel_map_binary() -> Result, &'static str> { - let phys_kernel_tables_base_addr = - platform::memory::mmu::kernel_translation_tables().write(|tables| { - tables.init().unwrap(); - tables.phys_base_address() - }); - - // SAFETY: Make a mistake and you're dead, gaijin! - unsafe { - platform::memory::mmu::kernel_map_binary()?; - } - - Ok(phys_kernel_tables_base_addr) -} - -/// Enable the MMU and data + instruction caching. -/// -/// # Safety -/// -/// - Crucial function during kernel init. Changes the the complete memory view of the processor. -#[inline] -pub unsafe fn enable_mmu_and_caching( - phys_tables_base_addr: Address, -) -> Result<(), MMUEnableError> { - // SAFETY: Make a mistake and you're dead, gaijin! - unsafe { arch_mmu::mmu().enable_mmu_and_caching(phys_tables_base_addr) } -} - -/// Finish initialization of the MMU subsystem. -#[inline] -pub fn post_enable_init() { - kernel_init_mmio_va_allocator(); -} - -/// Human-readable print of all recorded kernel mappings. -#[inline] -pub fn kernel_print_mappings() { - mapping_record::kernel_print(); -} diff --git a/libs/memory/src/mmu/unused.rs b/libs/memory/src/mmu/unused.rs deleted file mode 100644 index 897c0796f..000000000 --- a/libs/memory/src/mmu/unused.rs +++ /dev/null @@ -1,124 +0,0 @@ -//-------------------------------------------------------------------------------------------------- -// Laterrrr -//-------------------------------------------------------------------------------------------------- - -/// Architecture agnostic memory region translation types. -#[allow(dead_code)] -#[derive(Copy, Clone)] -pub enum Translation { - /// One-to-one address mapping - Identity, - /// Mapping with a specified offset - Offset(usize), -} - -//-------------------------------------------------------------------------------------------------- -// Public Definitions -//-------------------------------------------------------------------------------------------------- - -/// Types used for compiling the virtual memory layout of the kernel using address ranges. -/// -/// Memory region descriptor. -/// -/// Used to construct iterable kernel memory ranges. -pub struct TranslationDescriptor { - /// Name of the region - pub name: &'static str, - /// Virtual memory range - pub virtual_range: fn() -> RangeInclusive, - /// Mapping translation - pub physical_range_translation: Translation, - /// Attributes - pub attribute_fields: AttributeFields, -} - -/// Type for expressing the kernel's virtual memory layout. -pub struct KernelVirtualLayout { - /// The last (inclusive) address of the address space. - max_virt_addr_inclusive: usize, - - /// Array of descriptors for non-standard (normal cacheable DRAM) memory regions. - inner: [TranslationDescriptor; NUM_SPECIAL_RANGES], -} - -//-------------------------------------------------------------------------------------------------- -// Public Code -//-------------------------------------------------------------------------------------------------- - -/// Human-readable output of a Descriptor. -impl fmt::Display for TranslationDescriptor { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - // Call the function to which self.range points, and dereference the - // result, which causes Rust to copy the value. - let start = *(self.virtual_range)().start(); - let end = *(self.virtual_range)().end(); - let size = end - start + 1; - - // log2(1024) - const KIB_SHIFT: u32 = 10; - - // log2(1024 * 1024) - const MIB_SHIFT: u32 = 20; - - let (size, unit) = if (size >> MIB_SHIFT) > 0 { - (size >> MIB_SHIFT, "MiB") - } else if (size >> KIB_SHIFT) > 0 { - (size >> KIB_SHIFT, "KiB") - } else { - (size, "Byte") - }; - - write!( - f, - " {:#010x} - {:#010x} | {: >3} {} | {} | {}", - start, end, size, unit, self.attribute_fields, self.name - ) - } -} - -impl KernelVirtualLayout<{ NUM_SPECIAL_RANGES }> { - /// Create a new instance. - pub const fn new(max: usize, layout: [TranslationDescriptor; NUM_SPECIAL_RANGES]) -> Self { - Self { - max_virt_addr_inclusive: max, - inner: layout, - } - } - - /// For a given virtual address, find and return the output address and - /// corresponding attributes. - /// - /// If the address is not found in `inner`, return an identity mapped default for normal - /// cacheable DRAM. - pub fn virt_addr_properties( - &self, - virt_addr: usize, - ) -> Result<(usize, AttributeFields), &'static str> { - if virt_addr > self.max_virt_addr_inclusive { - return Err("Address out of range"); - } - - for i in self.inner.iter() { - if (i.virtual_range)().contains(&virt_addr) { - let output_addr = match i.physical_range_translation { - Translation::Identity => virt_addr, - Translation::Offset(a) => a + (virt_addr - (i.virtual_range)().start()), - }; - - return Ok((output_addr, i.attribute_fields)); - } - } - - Ok((virt_addr, AttributeFields::default())) - } - - /// Print the kernel memory layout. - pub fn print_layout(&self) { - println!("[i] Kernel memory layout:"); //info! - - for i in self.inner.iter() { - // for i in KERNEL_VIRTUAL_LAYOUT.iter() { - println!("{}", i); //info! - } - } -} diff --git a/libs/memory/src/mmu/page_alloc.rs b/libs/memory/src/page_alloc.rs similarity index 100% rename from libs/memory/src/mmu/page_alloc.rs rename to libs/memory/src/page_alloc.rs diff --git a/libs/memory/src/platform/README.md b/libs/memory/src/platform/README.md deleted file mode 100644 index 78eb5744e..000000000 --- a/libs/memory/src/platform/README.md +++ /dev/null @@ -1 +0,0 @@ -Temp, until libplatform is introduced. diff --git a/libs/memory/src/platform/mod.rs b/libs/memory/src/platform/mod.rs deleted file mode 100644 index ee8df103f..000000000 --- a/libs/memory/src/platform/mod.rs +++ /dev/null @@ -1,4 +0,0 @@ -pub type KernelGranule = crate::mmu::TranslationGranule<{ 64 * 1024 }>; - -pub mod raspberrypi; -pub use raspberrypi::*; diff --git a/libs/memory/src/platform/raspberrypi/memory/mmu.rs b/libs/memory/src/platform/raspberrypi/memory/mmu.rs deleted file mode 100644 index 1d41c99ca..000000000 --- a/libs/memory/src/platform/raspberrypi/memory/mmu.rs +++ /dev/null @@ -1,288 +0,0 @@ -//! Platform memory management unit. - -use { - crate::mmu::{ - AddressSpace, AssociatedTranslationTable, MemoryRegion, PageAddress, TranslationGranule, - }, - libaddress::{Physical, Virtual}, - liblocking::InitStateLock, -}; - -//-------------------------------------------------------------------------------------------------- -// Private Definitions -//-------------------------------------------------------------------------------------------------- - -type KernelTranslationTable = - ::TableStartFromBottom; - -//-------------------------------------------------------------------------------------------------- -// Public Definitions -//-------------------------------------------------------------------------------------------------- - -/// The translation granule chosen by this platform. This will be used everywhere else -/// in the kernel to derive respective data structures and their sizes. -/// For example, the `crate::memory::mmu::Page`. -pub type KernelGranule = TranslationGranule<{ 64 * 1024 }>; - -/// The kernel's virtual address space defined by this platform. -pub type KernelVirtAddrSpace = AddressSpace<{ 1024 * 1024 * 1024 }>; - -//-------------------------------------------------------------------------------------------------- -// Global instances -//-------------------------------------------------------------------------------------------------- - -/// The kernel translation tables. -/// -/// It is mandatory that `InitStateLock` is transparent. -/// That is, `size_of(InitStateLock) == size_of(KernelTranslationTable)`. -/// There is a unit tests that checks this property. -pub static KERNEL_TABLES: InitStateLock = - InitStateLock::new(KernelTranslationTable::new()); - -//-------------------------------------------------------------------------------------------------- -// Private Code -//-------------------------------------------------------------------------------------------------- - -/// Helper function for calculating the number of pages the given parameter spans. -const fn size_to_num_pages(size: usize) -> usize { - assert!(size > 0); - assert!(size.is_multiple_of(KernelGranule::SIZE)); // assert! is const-fn-friendly - - size >> KernelGranule::SHIFT -} - -/// The code pages of the kernel binary. -pub fn virt_code_region() -> MemoryRegion { - let num_pages = size_to_num_pages(super::code_size()); - - let start_page_addr = super::virt_code_start(); - let end_exclusive_page_addr = start_page_addr - .checked_page_offset(num_pages.cast_signed()) - .unwrap(); - - MemoryRegion::new(start_page_addr, end_exclusive_page_addr) -} - -/// The data pages of the kernel binary. -pub fn virt_data_region() -> MemoryRegion { - let num_pages = size_to_num_pages(super::data_size()); - - let start_page_addr = super::virt_data_start(); - let end_exclusive_page_addr = start_page_addr - .checked_page_offset(num_pages.cast_signed()) - .unwrap(); - - MemoryRegion::new(start_page_addr, end_exclusive_page_addr) -} - -/// The boot core stack pages. -pub fn virt_boot_core_stack_region() -> MemoryRegion { - let num_pages = size_to_num_pages(super::boot_core_stack_size()); - - let start_page_addr = super::virt_boot_core_stack_start(); - let end_exclusive_page_addr = start_page_addr - .checked_page_offset(num_pages.cast_signed()) - .unwrap(); - - MemoryRegion::new(start_page_addr, end_exclusive_page_addr) -} - -// The binary is still identity mapped, so use this trivial conversion function for mapping below. - -fn kernel_virt_to_phys_region(virt_region: MemoryRegion) -> MemoryRegion { - MemoryRegion::new( - PageAddress::from(virt_region.start_page_addr().into_inner().as_usize()), - PageAddress::from( - virt_region - .end_exclusive_page_addr() - .into_inner() - .as_usize(), - ), - ) -} - -//-------------------------------------------------------------------------------------------------- -// Subsumed by the kernel_map_binary() function -//-------------------------------------------------------------------------------------------------- - -// These are part of a static linked image and used for proper kernel-space initialization. -// i.e. these data are subtracted from the dtb-provided memory map. -// pub static LAYOUT: KernelVirtualLayout = KernelVirtualLayout::new( -// memory_map::END_INCLUSIVE, -// [ -// TranslationDescriptor { -// name: "Remapped Device MMIO", -// virtual_range: remapped_mmio_range_inclusive, -// physical_range_translation: Translation::Offset( -// memory_map::mmio::MMIO_BASE + 0x20_0000, -// ), -// attribute_fields: AttributeFields { -// mem_attributes: MemAttributes::Device, -// acc_perms: AccessPermissions::ReadWrite, -// execute_never: true, -// }, -// }, -// @todo these should come from DTB and mem-map? -// TranslationDescriptor { -// name: "Device MMIO", -// virtual_range: mmio_range_inclusive, -// physical_range_translation: Translation::Identity, -// attribute_fields: AttributeFields { -// mem_attributes: MemAttributes::Device, -// acc_perms: AccessPermissions::ReadWrite, -// execute_never: true, -// }, -// }, -// @todo these should come from DTB and mem-map? -// TranslationDescriptor { -// name: "DMA heap pool", -// virtual_range: dma_range_inclusive, -// physical_range_translation: Translation::Identity, -// attribute_fields: AttributeFields { -// mem_attributes: MemAttributes::NonCacheableDRAM, -// acc_perms: AccessPermissions::ReadWrite, -// execute_never: true, -// }, -// }, -// TranslationDescriptor { -// name: "Framebuffer area (static for now)", -// virtual_range: || { -// RangeInclusive::new( -// memory_map::phys::VIDEOMEM_BASE, -// memory_map::mmio::MMIO_BASE - 1, -// ) -// }, -// physical_range_translation: Translation::Identity, -// attribute_fields: AttributeFields { -// mem_attributes: MemAttributes::Device, -// acc_perms: AccessPermissions::ReadWrite, -// execute_never: true, -// }, -// }, -// ], -// ); - -//-------------------------------------------------------------------------------------------------- -// Public Code -//-------------------------------------------------------------------------------------------------- - -/// Return a reference to the kernel's translation tables. -pub fn kernel_translation_tables() -> &'static InitStateLock { - &KERNEL_TABLES -} - -/// The MMIO remap pages. -pub fn virt_mmio_remap_region() -> MemoryRegion { - let num_pages = size_to_num_pages(super::mmio_remap_size()); - - let start_page_addr = super::virt_mmio_remap_start(); - let end_exclusive_page_addr = start_page_addr - .checked_page_offset(num_pages.cast_signed()) - .unwrap(); - - MemoryRegion::new(start_page_addr, end_exclusive_page_addr) -} - -/// Map the kernel binary. -/// -/// # Safety -/// -/// - Any miscalculation or attribute error will likely be fatal. Needs careful manual checking. -pub unsafe fn kernel_map_binary() -> Result<(), &'static str> { - // SAFETY: Make a mistake and you're dead, gaijin! - // unsafe { - // generic_mmu::kernel_map_at( - // "Kernel boot-core stack", - // &virt_boot_core_stack_region(), - // &kernel_virt_to_phys_region(virt_boot_core_stack_region()), - // AttributeFields { - // mem_attributes: MemAttributes::CacheableDRAM, - // acc_perms: AccessPermissions::ReadWrite, - // execute_never: true, - // }, - // )?; - // } - - // TranslationDescriptor { - // name: "Boot code and data", - // virtual_range: boot_range_inclusive, - // physical_range_translation: Translation::Identity, - // attribute_fields: AttributeFields { - // mem_attributes: MemAttributes::CacheableDRAM, - // acc_perms: AccessPermissions::ReadOnly, - // execute_never: false, - // }, - // }, - - // TranslationDescriptor { - // name: "Kernel code and RO data", - // virtual_range: code_range_inclusive, - // physical_range_translation: Translation::Identity, - // attribute_fields: AttributeFields { - // mem_attributes: MemAttributes::CacheableDRAM, - // acc_perms: AccessPermissions::ReadOnly, - // execute_never: false, - // }, - // }, - - // SAFETY: Make a mistake and you're dead, gaijin! - // unsafe { - // generic_mmu::kernel_map_at( - // "Kernel code and RO data", - // &virt_code_region(), - // &kernel_virt_to_phys_region(virt_code_region()), - // AttributeFields { - // mem_attributes: MemAttributes::CacheableDRAM, - // acc_perms: AccessPermissions::ReadOnly, - // execute_never: false, - // }, - // )?; - // } - - // SAFETY: Make a mistake and you're dead, gaijin! - // unsafe { - // generic_mmu::kernel_map_at( - // "Kernel data and bss", - // &virt_data_region(), - // &kernel_virt_to_phys_region(virt_data_region()), - // AttributeFields { - // mem_attributes: MemAttributes::CacheableDRAM, - // acc_perms: AccessPermissions::ReadWrite, - // execute_never: true, - // }, - // )?; - // } - - Ok(()) -} - -//-------------------------------------------------------------------------------------------------- -// Private Code -//-------------------------------------------------------------------------------------------------- - -// fn boot_range_inclusive() -> RangeInclusive { -// RangeInclusive::new(super::boot_start(), super::boot_end_exclusive() - 1) -// } -// -// fn code_range_inclusive() -> RangeInclusive { -// // Notice the subtraction to turn the exclusive end into an inclusive end. -// #[allow(clippy::range_minus_one)] -// RangeInclusive::new(super::code_start(), super::code_end_exclusive() - 1) -// } -// -// fn remapped_mmio_range_inclusive() -> RangeInclusive { -// // The last 64 KiB slot in the first 512 MiB -// RangeInclusive::new(0x1FFF_0000, 0x1FFF_FFFF) -// } -// -// fn mmio_range_inclusive() -> RangeInclusive { -// RangeInclusive::new(memory_map::mmio::MMIO_BASE, memory_map::mmio::MMIO_END) -// // RangeInclusive::new(map::phys::VIDEOMEM_BASE, map::mmio::MMIO_END), -// } -// -// fn dma_range_inclusive() -> RangeInclusive { -// RangeInclusive::new( -// memory_map::virt::DMA_HEAP_START, -// memory_map::virt::DMA_HEAP_END, -// ) -// } diff --git a/libs/memory/src/platform/raspberrypi/memory/mod.rs b/libs/memory/src/platform/raspberrypi/memory/mod.rs deleted file mode 100644 index 2ef4dfb85..000000000 --- a/libs/memory/src/platform/raspberrypi/memory/mod.rs +++ /dev/null @@ -1,370 +0,0 @@ -//! Platform memory Management. -//! -//! The physical memory layout. -//! -//! The Raspberry's firmware copies the kernel binary to `0x8_0000`. The preceding region will be used -//! as the boot core's stack. -//! -//! +---------------------------------------+ -//! | | `boot_core_stack_start` @ 0x0 -//! | | ^ -//! | Boot-core Stack | | stack -//! | | | growth -//! | | | direction -//! +---------------------------------------+ -//! | | `code_start` @ `0x8_0000` == `boot_core_stack_end_exclusive` -//! | .text | -//! | .rodata | -//! | .got | -//! | | -//! +---------------------------------------+ -//! | | `data_start` == `code_end_exclusive` -//! | .data | -//! | .bss | -//! | | -//! +---------------------------------------+ -//! | | `data_end_exclusive` -//! | | -//! -//! -//! -//! -//! -//! The virtual memory layout is as follows: -//! -//! +---------------------------------------+ -//! | | `boot_core_stack_start` @ 0x0 -//! | | ^ -//! | Boot-core Stack | | stack -//! | | | growth -//! | | | direction -//! +---------------------------------------+ -//! | | `code_start` @ `0x8_0000` == `boot_core_stack_end_exclusive` -//! | .text | -//! | .rodata | -//! | .got | -//! | | -//! +---------------------------------------+ -//! | | `data_start` == `code_end_exclusive` -//! | .data | -//! | .bss | -//! | | -//! +---------------------------------------+ -//! | | `mmio_remap_start` == `data_end_exclusive` -//! | VA region for MMIO remapping | -//! | | -//! +---------------------------------------+ -//! | | `mmio_remap_end_exclusive` -//! | | -pub mod mmu; - -//-------------------------------------------------------------------------------------------------- -// Private Definitions -//-------------------------------------------------------------------------------------------------- - -use { - crate::mmu::PageAddress, - core::cell::UnsafeCell, - libaddress::{Address, Physical, Virtual}, -}; - -// Symbols from the linker script. -unsafe extern "Rust" { - // Boot code. - // - // Using the linker script, we ensure that the boot area is consecutive and 4 - // KiB aligned, and we export the boundaries via symbols: - // - // [__BOOT_START, __BOOT_END) - // - // The inclusive start of the boot area, aka the address of the - // first byte of the area. - static __BOOT_START: UnsafeCell<()>; - - // The exclusive end of the boot area, aka the address of - // the first byte _after_ the RO area. - static __BOOT_END: UnsafeCell<()>; - - // Kernel code and RO data. - // - // Using the linker script, we ensure that the RO area is consecutive and 4 - // KiB aligned, and we export the boundaries via symbols: - // - // [__RO_START, __RO_END) - // - // The inclusive start of the read-only area, aka the address of the - // first byte of the area. - static __CODE_START: UnsafeCell<()>; - // The exclusive end of the read-only area, aka the address of - // the first byte _after_ the RO area. - static __CODE_END: UnsafeCell<()>; - - // The inclusive start of the kernel data/BSS area, aka the address of the - // first byte of the area. - static __DATA_START: UnsafeCell<()>; - // The exclusive end of the kernel data/BSS area, aka the address of - // the first byte _after_ the data/BSS area. - static __DATA_END: UnsafeCell<()>; - - // The inclusive start of the kernel data/BSS area, aka the address of the - // first byte of the area. - static __STACK_BOTTOM: UnsafeCell<()>; - // The exclusive end of the kernel data/BSS area, aka the address of - // the first byte _after_ the data/BSS area. - static __STACK_TOP: UnsafeCell<()>; - - // The inclusive start of the kernel MMIO remap area, aka the address of the - // first byte of the area. - static __MMIO_REMAP_START: UnsafeCell<()>; - // The exclusive end of the kernel MMIO remap area, aka the address of - // the first byte _after_ the MMIO remap area. - static __MMIO_REMAP_END: UnsafeCell<()>; -} - -//-------------------------------------------------------------------------------------------------- -// Public Definitions -//-------------------------------------------------------------------------------------------------- - -/// The board's physical memory map. -/// This is a fixed memory map for Raspberry Pi, -/// @todo we need to infer the memory map from the provided DTB instead. -#[rustfmt::skip] -pub mod map { - use super::*; - - /// Beginning of memory. - pub const START: usize = 0x0000_0000; - /// End of memory - 8Gb `RPi4` - pub const END_INCLUSIVE: usize = 0x1_FFFF_FFFF; - - /// Physical RAM addresses. - pub mod phys { - /// Base address of video (VC) memory. - pub const VIDEOMEM_BASE: usize = 0x3e00_0000; - } - - pub const VIDEOCORE_MBOX_OFFSET: usize = 0x0000_B880; - pub const POWER_OFFSET: usize = 0x0010_0000; - pub const GPIO_OFFSET: usize = 0x0020_0000; - pub const UART_OFFSET: usize = 0x0020_1000; - pub const MINIUART_OFFSET: usize = 0x0021_5000; - - /// Physical devices. - #[cfg(board_rpi3)] - pub mod mmio { - use super::*; - - /// Base address of MMIO register range. - pub const MMIO_BASE: usize = 0x3F00_0000; - - /// Interrupt controller - pub const PERIPHERAL_IC_BASE: Address = Address::new((MMIO_BASE + 0x0000_B200) as u64); - pub const PERIPHERAL_IC_SIZE: usize = 0x24; - - /// Base address of ARM<->VC mailbox area. - pub const VIDEOCORE_MBOX_BASE: Address = Address::new((MMIO_BASE + VIDEOCORE_MBOX_OFFSET) as u64); - - /// Board power control. - pub const POWER_BASE: Address = Address::new((MMIO_BASE + POWER_OFFSET) as u64); - - /// Base address of GPIO registers. - pub const GPIO_BASE: Address = Address::new((MMIO_BASE + GPIO_OFFSET) as u64); - pub const GPIO_SIZE: usize = 0xA0; - - pub const PL011_UART_BASE: Address = Address::new((MMIO_BASE + UART_OFFSET) as u64); - pub const PL011_UART_SIZE: usize = 0x48; - - /// Base address of `MiniUART`. - pub const MINI_UART_BASE: Address = Address::new((MMIO_BASE + MINIUART_OFFSET) as u64); - - /// End of MMIO memory region. - pub const END: Address = Address::new(0x4001_0000); - } - - /// Physical devices. - #[cfg(board_rpi4)] - pub mod mmio { - use super::*; - - /// Base address of MMIO register range. - pub const MMIO_BASE: usize = 0xFE00_0000; - - /// Base address of GPIO registers. - pub const GPIO_BASE: Address = Address::new(MMIO_BASE + GPIO_OFFSET); - pub const GPIO_SIZE: usize = 0xA0; - - /// Base address of regular UART. - pub const PL011_UART_BASE: Address = Address::new(MMIO_BASE + UART_OFFSET); - pub const PL011_UART_SIZE: usize = 0x48; - - /// Base address of `MiniUART`. - pub const MINI_UART_BASE: Address = Address::new(MMIO_BASE + MINIUART_OFFSET); - - /// Interrupt controller - pub const GICD_BASE: Address = Address::new(0xFF84_1000); - pub const GICD_SIZE: usize = 0x824; - - pub const GICC_BASE: Address = Address::new(0xFF84_2000); - pub const GICC_SIZE: usize = 0x14; - - /// Base address of ARM<->VC mailbox area. - pub const VIDEOCORE_MBOX_BASE: usize = MMIO_BASE + VIDEOCORE_MBOX_OFFSET; - - /// End of MMIO memory region. - pub const END: Address = Address::new(0xFF85_0000); - } - - #[cfg(not(any(board_rpi3, board_rpi4)))] - compile_error!("No platform selected - specify TARGET_BOARD in configuration"); - - /// End address of mapped memory. - pub const END: Address = mmio::END; - - //---- - // Unused? - //---- - - /// Virtual (mapped) addresses. - pub mod virt { - /// Start (top) of kernel stack. - pub const KERN_STACK_START: usize = super::START; - /// End (bottom) of kernel stack. SP starts at `KERN_STACK_END` + 1. - pub const KERN_STACK_END: usize = 0x0007_FFFF; - - /// Location of DMA-able memory region (in the second 2 MiB block). - pub const DMA_HEAP_START: usize = 0x0020_0000; - /// End of DMA-able memory region. - pub const DMA_HEAP_END: usize = 0x005F_FFFF; - } -} - -//-------------------------------------------------------------------------------------------------- -// Private Code -//-------------------------------------------------------------------------------------------------- - -/// Start page address of the boot segment. -/// -/// # Safety -/// -/// - Value is provided by the linker script and must be trusted as-is. -#[inline(always)] -fn boot_start() -> usize { - // SAFETY: The linker script ensures the boot code section has a sensible start address. - unsafe { __BOOT_START.get() as usize } -} - -/// Exclusive end page address of the boot segment. -/// # Safety -/// -/// - Value is provided by the linker script and must be trusted as-is. -#[inline(always)] -fn boot_end_exclusive() -> usize { - // SAFETY: The linker script ensures the boot code section has a sensible end address. - unsafe { __BOOT_END.get() as usize } -} - -/// Start page address of the code segment. -/// -/// # Safety -/// -/// - Value is provided by the linker script and must be trusted as-is. -#[inline(always)] -fn code_start() -> usize { - // SAFETY: The linker script ensures the code section has a sensible start address. - unsafe { __CODE_START.get() as usize } -} - -/// Start page address of the code segment. -/// -/// # Safety -/// -/// - Value is provided by the linker script and must be trusted as-is. -#[inline(always)] -fn virt_code_start() -> PageAddress { - // SAFETY: The linker script ensures the code section has a sensible start address. - PageAddress::from(unsafe { __CODE_START.get() as usize }) -} - -/// Size of the code segment. -/// -/// # Safety -/// -/// - Value is provided by the linker script and must be trusted as-is. -#[inline(always)] -fn code_size() -> usize { - // SAFETY: The linker script ensures the code section has a sensible size. - unsafe { (__CODE_END.get() as usize) - (__CODE_START.get() as usize) } -} - -// Exclusive end page address of the code segment. -// # Safety -// -// - Value is provided by the linker script and must be trusted as-is. -// #[inline(always)] -// fn code_end_exclusive() -> usize { -// unsafe { __RO_END.get() as usize } -// } - -/// Start page address of the data segment. -#[inline(always)] -fn virt_data_start() -> PageAddress { - // SAFETY: The linker script ensures the data section has a sensible start address. - PageAddress::from(unsafe { __DATA_START.get() as usize }) -} - -/// Size of the data segment. -/// -/// # Safety -/// -/// - Value is provided by the linker script and must be trusted as-is. -#[inline(always)] -fn data_size() -> usize { - // SAFETY: The linker script ensures the data section has a sensible size. - unsafe { (__DATA_END.get() as usize) - (__DATA_START.get() as usize) } -} - -/// Start page address of the MMIO remap reservation. -/// -/// # Safety -/// -/// - Value is provided by the linker script and must be trusted as-is. -#[inline(always)] -fn virt_mmio_remap_start() -> PageAddress { - // SAFETY: The linker script ensures the MMIO area has a sensible start address. - PageAddress::from(unsafe { __MMIO_REMAP_START.get() as usize }) -} - -/// Size of the MMIO remap reservation. -/// -/// # Safety -/// -/// - Value is provided by the linker script and must be trusted as-is. -#[inline(always)] -fn mmio_remap_size() -> usize { - // SAFETY: The linker script ensures the MMIO area has a sensible size. - unsafe { (__MMIO_REMAP_END.get() as usize) - (__MMIO_REMAP_START.get() as usize) } -} - -/// Start page address of the boot core's stack. -#[inline(always)] -fn virt_boot_core_stack_start() -> PageAddress { - // SAFETY: The linker script ensures the stack bottom has a sensible address. - PageAddress::from(unsafe { __STACK_BOTTOM.get() as usize }) -} - -/// Size of the boot core's stack. -#[inline(always)] -fn boot_core_stack_size() -> usize { - // SAFETY: The linker script ensures the stack has a sensible size. - unsafe { (__STACK_TOP.get() as usize) - (__STACK_BOTTOM.get() as usize) } -} - -//-------------------------------------------------------------------------------------------------- -// Public Code -//-------------------------------------------------------------------------------------------------- - -/// Exclusive end address of the physical address space. -#[inline(always)] -pub fn phys_addr_space_end_exclusive_addr() -> PageAddress { - PageAddress::from(map::END) -} diff --git a/libs/memory/src/platform/raspberrypi/mod.rs b/libs/memory/src/platform/raspberrypi/mod.rs deleted file mode 100644 index eb291915f..000000000 --- a/libs/memory/src/platform/raspberrypi/mod.rs +++ /dev/null @@ -1 +0,0 @@ -pub mod memory; diff --git a/libs/memory/src/mmu/translation_table.rs b/libs/memory/src/translation_table.rs similarity index 100% rename from libs/memory/src/mmu/translation_table.rs rename to libs/memory/src/translation_table.rs diff --git a/libs/memory/src/mmu/types.rs b/libs/memory/src/types.rs similarity index 100% rename from libs/memory/src/mmu/types.rs rename to libs/memory/src/types.rs diff --git a/libs/platform/src/raspberrypi/memory.rs b/libs/platform/src/raspberrypi/memory.rs new file mode 100644 index 000000000..1d5588eb9 --- /dev/null +++ b/libs/platform/src/raspberrypi/memory.rs @@ -0,0 +1,146 @@ +//! Platform memory Management. +//! +//! The physical memory layout. +//! +//! The Raspberry's firmware copies the kernel binary to `0x8_0000`. The preceding region will be used +//! as the boot core's stack. +//! + +use { + crate::mmu::{ + AddressSpace, AssociatedTranslationTable, MemoryRegion, PageAddress, TranslationGranule, + }, + libaddress::PhysAddr, +}; + +//-------------------------------------------------------------------------------------------------- +// Private Definitions +//-------------------------------------------------------------------------------------------------- + +type KernelTranslationTable = + ::TableStartFromBottom; + +//-------------------------------------------------------------------------------------------------- +// Public Definitions +//-------------------------------------------------------------------------------------------------- + +/// The translation granule chosen by this platform. This will be used everywhere else +/// in the kernel to derive respective data structures and their sizes. +/// For example, the `crate::memory::mmu::Page`. +pub type KernelGranule = TranslationGranule<{ 64 * 1024 }>; + +/// The kernel's virtual address space defined by this platform. +pub type KernelVirtAddrSpace = AddressSpace<{ 1024 * 1024 * 1024 }>; + +/// The board's physical memory map. +/// This is a fixed memory map for Raspberry Pi, +/// @todo we need to infer the memory map from the provided DTB instead. +#[rustfmt::skip] +pub mod map { + use super::*; + + /// Beginning of memory. + pub const START: usize = 0x0000_0000; + /// End of memory - 8Gb `RPi4` + pub const END_INCLUSIVE: usize = 0x1_FFFF_FFFF; + + /// Physical RAM addresses. + pub mod phys { + /// Base address of video (VC) memory. + pub const VIDEOMEM_BASE: usize = 0x3e00_0000; + } + + pub const VIDEOCORE_MBOX_OFFSET: usize = 0x0000_B880; + pub const POWER_OFFSET: usize = 0x0010_0000; + pub const GPIO_OFFSET: usize = 0x0020_0000; + pub const UART_OFFSET: usize = 0x0020_1000; + pub const MINIUART_OFFSET: usize = 0x0021_5000; + + /// Physical devices. + #[cfg(board_rpi3)] + pub mod mmio { + use super::*; + + /// Base address of MMIO register range. + pub const MMIO_BASE: usize = 0x3F00_0000; + + /// Interrupt controller + pub const PERIPHERAL_IC_BASE: PhysAddr = PhysAddr::new((MMIO_BASE + 0x0000_B200) as u64); + pub const PERIPHERAL_IC_SIZE: usize = 0x24; + + /// Base address of ARM<->VC mailbox area. + pub const VIDEOCORE_MBOX_BASE: PhysAddr = PhysAddr::new((MMIO_BASE + VIDEOCORE_MBOX_OFFSET) as u64); + + /// Board power control. + pub const POWER_BASE: PhysAddr = PhysAddr::new((MMIO_BASE + POWER_OFFSET) as u64); + + /// Base address of GPIO registers. + pub const GPIO_BASE: PhysAddr = PhysAddr::new((MMIO_BASE + GPIO_OFFSET) as u64); + pub const GPIO_SIZE: usize = 0xA0; + + pub const PL011_UART_BASE: PhysAddr = PhysAddr::new((MMIO_BASE + UART_OFFSET) as u64); + pub const PL011_UART_SIZE: usize = 0x48; + + /// Base address of `MiniUART`. + pub const MINI_UART_BASE: PhysAddr = PhysAddr::new((MMIO_BASE + MINIUART_OFFSET) as u64); + + /// End of MMIO memory region. + pub const END: PhysAddr = PhysAddr::new(0x4001_0000); + } + + /// Physical devices. + #[cfg(board_rpi4)] + pub mod mmio { + use super::*; + + /// Base address of MMIO register range. + pub const MMIO_BASE: usize = 0xFE00_0000; + + /// Base address of GPIO registers. + pub const GPIO_BASE: PhysAddr = PhysAddr::new(MMIO_BASE + GPIO_OFFSET); + pub const GPIO_SIZE: usize = 0xA0; + + /// Base address of regular UART. + pub const PL011_UART_BASE: PhysAddr = PhysAddr::new(MMIO_BASE + UART_OFFSET); + pub const PL011_UART_SIZE: usize = 0x48; + + /// Base address of `MiniUART`. + pub const MINI_UART_BASE: PhysAddr = PhysAddr::new(MMIO_BASE + MINIUART_OFFSET); + + /// Interrupt controller + pub const GICD_BASE: PhysAddr = PhysAddr::new(0xFF84_1000); + pub const GICD_SIZE: usize = 0x824; + + pub const GICC_BASE: PhysAddr = PhysAddr::new(0xFF84_2000); + pub const GICC_SIZE: usize = 0x14; + + /// Base address of ARM<->VC mailbox area. + pub const VIDEOCORE_MBOX_BASE: usize = MMIO_BASE + VIDEOCORE_MBOX_OFFSET; + + /// End of MMIO memory region. + pub const END: PhysAddr = PhysAddr::new(0xFF85_0000); + } + + #[cfg(not(any(board_rpi3, board_rpi4)))] + compile_error!("No platform selected - specify TARGET_BOARD in configuration"); + + /// End address of mapped memory. + pub const END: PhysAddr = mmio::END; + + //---- + // Unused? + //---- + + /// Virtual (mapped) addresses. + pub mod virt { + /// Start (top) of kernel stack. + pub const KERN_STACK_START: usize = super::START; + /// End (bottom) of kernel stack. SP starts at `KERN_STACK_END` + 1. + pub const KERN_STACK_END: usize = 0x0007_FFFF; + + /// Location of DMA-able memory region (in the second 2 MiB block). + pub const DMA_HEAP_START: usize = 0x0020_0000; + /// End of DMA-able memory region. + pub const DMA_HEAP_END: usize = 0x005F_FFFF; + } +} diff --git a/libs/platform/src/raspberrypi/mod.rs b/libs/platform/src/raspberrypi/mod.rs index 2f0d59f61..60175723b 100644 --- a/libs/platform/src/raspberrypi/mod.rs +++ b/libs/platform/src/raspberrypi/mod.rs @@ -11,6 +11,7 @@ pub mod display; pub mod drivers; pub mod exception; pub mod fb; +pub mod memory; pub mod vc; /// See BCM2835-ARM-Peripherals.pdf From f6d6057dbe72650693153812f236fbfba50dcdf9 Mon Sep 17 00:00:00 2001 From: Berkus Decker Date: Sun, 8 Feb 2026 15:23:34 +0200 Subject: [PATCH 070/107] =?UTF-8?q?refactor:=20=E2=99=BB=EF=B8=8F=20Split?= =?UTF-8?q?=20libmemory=20to=20libmmu,=20liballoc,=20libmapping,=20libmmio?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Cargo.toml | 7 + kernel/Plan.md | 2 + kernel/init_thread/src/main.rs | 2 +- kernel/tests/memory.rs | 9 +- kernel/tests/platform.rs | 6 +- libs/log/src/lib.rs | 18 + libs/mapping/Cargo.toml | 29 ++ libs/mapping/README.md | 2 + libs/mapping/src/lib.rs | 120 +++++++ libs/mapping/src/mapping_attributes.rs | 66 ++++ .../{memory => mapping}/src/mapping_record.rs | 99 +++--- libs/mapping/src/memory_region.rs | 140 ++++++++ libs/mapping/src/page_address.rs | 86 +++++ libs/memory/Cargo.toml | 1 + libs/memory/README.md | 5 + .../src/arch/aarch64/area_frame_allocator.rs | 106 ------ .../src/arch/aarch64/{mmu/mod.rs => mmu.rs} | 8 +- libs/memory/src/arch/aarch64/mod.rs | 7 +- .../aarch64/{mmu => }/translation_table.rs | 27 +- libs/memory/src/lib.rs | 180 +--------- libs/memory/src/page_alloc.rs | 3 +- libs/memory/src/translation_table.rs | 9 +- libs/memory/src/types.rs | 334 ------------------ libs/mmio/Cargo.toml | 26 ++ libs/mmio/README.md | 3 + .../common.rs => mmio/src/lib.rs} | 6 +- libs/mmio/src/mmio_descriptor.rs | 31 ++ libs/platform/Cargo.toml | 4 +- .../device_driver/arm/gicv2/gicc.rs | 2 +- .../device_driver/arm/gicv2/gicd.rs | 2 +- .../device_driver/arm/gicv2/mod.rs | 2 +- .../raspberrypi/device_driver/bcm/mailbox.rs | 2 +- .../src/raspberrypi/device_driver/mod.rs | 2 - libs/platform/src/raspberrypi/drivers.rs | 11 +- userspace/README.md | 1 + userspace/shell.rs | 2 +- 36 files changed, 645 insertions(+), 715 deletions(-) create mode 100644 libs/mapping/Cargo.toml create mode 100644 libs/mapping/README.md create mode 100644 libs/mapping/src/lib.rs create mode 100644 libs/mapping/src/mapping_attributes.rs rename libs/{memory => mapping}/src/mapping_record.rs (72%) create mode 100644 libs/mapping/src/memory_region.rs create mode 100644 libs/mapping/src/page_address.rs delete mode 100644 libs/memory/src/arch/aarch64/area_frame_allocator.rs rename libs/memory/src/arch/aarch64/{mmu/mod.rs => mmu.rs} (99%) rename libs/memory/src/arch/aarch64/{mmu => }/translation_table.rs (96%) delete mode 100644 libs/memory/src/types.rs create mode 100644 libs/mmio/Cargo.toml create mode 100644 libs/mmio/README.md rename libs/{platform/src/raspberrypi/device_driver/common.rs => mmio/src/lib.rs} (94%) create mode 100644 libs/mmio/src/mmio_descriptor.rs create mode 100644 userspace/README.md diff --git a/Cargo.toml b/Cargo.toml index cf2ed4688..1c5bd0d63 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,6 +6,7 @@ members = [ "bin/chainofcommand", # Libraries "libs/address", + "libs/alloc", "libs/boot", "libs/console", "libs/cpu", @@ -16,7 +17,9 @@ members = [ "libs/locking", "libs/log", "libs/machine", + "libs/mapping", "libs/memory", + "libs/mmio", "libs/object", "libs/platform", "libs/primitives", @@ -51,6 +54,7 @@ bitflags = { version = "2.10" } buddy-alloc = { version = "0.6.0", git = "https://github.com/metta-systems/buddy-alloc", branch = "feature/allocator-api" } cfg-if = { version = "1.0" } libaddress = { path = "libs/address" } +liballoc = { path = "libs/alloc" } libboot = { path = "libs/boot" } libconsole = { path = "libs/console" } libcpu = { path = "libs/cpu" } @@ -61,7 +65,9 @@ liblocal-irq = { path = "libs/local-irq" } liblocking = { path = "libs/locking" } liblog = { path = "libs/log" } libmachine = { path = "libs/machine" } +libmapping = { path = "libs/mapping" } libmemory = { path = "libs/memory" } +libmmio = { path = "libs/mmio" } libobject = { path = "libs/object" } libplatform = { path = "libs/platform" } libprimitives = { path = "libs/primitives" } @@ -73,6 +79,7 @@ libtime = { path = "libs/time" } num = { version = "0.4", default-features = false } once_cell = { version = "1.21", default-features = false, features = ["unstable"] } qemu-exit = { version = "3.0" } +safe-mmio = { version = "0.2" } snafu = { version = "0.8", default-features = false, features = ["unstable-core-error"] } static_assertions = { version = "1.1.0" } tock-registers = { version = "0.10" } diff --git a/kernel/Plan.md b/kernel/Plan.md index 0ca7f0d7f..a67128107 100644 --- a/kernel/Plan.md +++ b/kernel/Plan.md @@ -19,6 +19,8 @@ Steps: - [x] Fill it with capability to DebugConsole - [x] Invoke DebugConsole.Write via syscall +- [ ] Generate complete memory map from DTB, print it out + - [ ] Make some caps work - Untypeds, Domains, Buffers, what else? - [ ] Test out syscalls from EL0 diff --git a/kernel/init_thread/src/main.rs b/kernel/init_thread/src/main.rs index d0c495cee..512e0acd1 100644 --- a/kernel/init_thread/src/main.rs +++ b/kernel/init_thread/src/main.rs @@ -532,7 +532,7 @@ pub fn init_thread_run() -> ! { // switch_to_domain(init_domain, init_time); libqemu::semihosting::exit_success() - // libmemory::mmu::post_enable_init(); // kernel_init_mmio_va_allocator + // kernel_init_mmio_va_allocator() // SAFETY: Not safe! // if let Err(x) = unsafe { libplatform::platform::drivers::init() } { diff --git a/kernel/tests/memory.rs b/kernel/tests/memory.rs index 7c5075a6b..096c96d75 100644 --- a/kernel/tests/memory.rs +++ b/kernel/tests/memory.rs @@ -16,25 +16,22 @@ use { num::NonZeroUsize, ops::Range, }, + libaddress::{align_up, Address, PhysAddr, PhysAddrNotValid, Physical, Virtual}, + liballoc::BumpAllocator, liblocking::interface::Mutex, liblog::println, libmemory::{ arch::mmu::translation_table::{PageDescriptor, TableDescriptor}, - mm::{align_up, BumpAllocator}, mmu::{ kernel_map_at, page_alloc, translation_table::{interface::TranslationTable, FixedSizeTranslationTable}, AccessPermissions, AttributeFields, MemAttributes, MemoryRegion, PageAddress, }, - phys_addr::{PhysAddr, PhysAddrNotValid}, platform::memory::mmu::{ virt_boot_core_stack_region, virt_code_region, virt_data_region, KERNEL_TABLES, }, - platform::KernelGranule, //memory::mmu::KernelGranule}, - Address, - Physical, - Virtual, }, + libplatform::KernelGranule, }; mod common; diff --git a/kernel/tests/platform.rs b/kernel/tests/platform.rs index 3bd745dad..ecf15c8c3 100644 --- a/kernel/tests/platform.rs +++ b/kernel/tests/platform.rs @@ -5,10 +5,10 @@ #![reexport_test_harness_main = "test_main"] use { - libmemory::{Address, Virtual}, + libaddress::{Address, Virtual}, libplatform::platform::device_driver::{ - Function, GPIO, RateDivisors, - mailbox::{self, Mailbox, tag}, + mailbox::{self, tag, Mailbox}, + Function, RateDivisors, GPIO, }, }; diff --git a/libs/log/src/lib.rs b/libs/log/src/lib.rs index 12cf21c21..fd4833cf9 100644 --- a/libs/log/src/lib.rs +++ b/libs/log/src/lib.rs @@ -527,3 +527,21 @@ pub fn logger() -> &'static dyn Log { &NOP } } + +/// Convert a size into human readable format. +/// FIXME: A candidate for libdebug? +pub const fn size_human_readable_ceil(size: usize) -> (usize, &'static str) { + const KIB: usize = 1024; + const MIB: usize = 1024 * 1024; + const GIB: usize = 1024 * 1024 * 1024; + + if (size / GIB) > 0 { + (size.div_ceil(GIB), "GiB") + } else if (size / MIB) > 0 { + (size.div_ceil(MIB), "MiB") + } else if (size / KIB) > 0 { + (size.div_ceil(KIB), "KiB") + } else { + (size, "Byte") + } +} diff --git a/libs/mapping/Cargo.toml b/libs/mapping/Cargo.toml new file mode 100644 index 000000000..041088f50 --- /dev/null +++ b/libs/mapping/Cargo.toml @@ -0,0 +1,29 @@ +[package] +name = "libmapping" +description = "Vesper nanokernel mapping abstractions." +authors = { workspace = true } +categories = { workspace = true } +documentation = { workspace = true } +edition = { workspace = true } +homepage = { workspace = true } +license = { workspace = true } +readme = { workspace = true } +repository = { workspace = true } +version = { workspace = true } +publish = false + +[badges] +maintenance = { status = "experimental" } + +[dependencies] +libaddress = { workspace = true } +liblocking = { workspace = true } +liblog = { workspace = true } +libmmio = { workspace = true } + +# Tests are offloaded to kernel integration tests binary. +[lib] +test = false + +[lints] +workspace = true diff --git a/libs/mapping/README.md b/libs/mapping/README.md new file mode 100644 index 000000000..1e221b251 --- /dev/null +++ b/libs/mapping/README.md @@ -0,0 +1,2 @@ +This lib should actually be called libmapping and should handle mapping record +tracking for maps/unmaps done from privileged processes. diff --git a/libs/mapping/src/lib.rs b/libs/mapping/src/lib.rs new file mode 100644 index 000000000..abc4abe68 --- /dev/null +++ b/libs/mapping/src/lib.rs @@ -0,0 +1,120 @@ +#![no_std] +#![feature(const_trait_impl)] +#![feature(step_trait)] +#![feature(new_range_api)] + +use { + libaddress::{Address, Virtual}, + libmmio::MMIODescriptor, +}; + +mod mapping_attributes; +mod mapping_record; +mod memory_region; +mod page_address; + +pub use {mapping_attributes::*, memory_region::*, page_address::*}; + +//-------------------------------------------------------------------------------------------------- +// Public Code +// FIXME: this code should move to init_thread and be only used during boot-up! (see paging.rs) +//-------------------------------------------------------------------------------------------------- + +/// Raw mapping of a virtual to physical region in the kernel translation tables. +/// +/// Prevents mapping into the MMIO range of the tables. +/// +/// # Safety +/// +/// - See `kernel_map_at_unchecked()`. +/// - Does not prevent aliasing. Currently, the callers must be trusted. +// pub unsafe fn kernel_map_at( +// name: &'static str, +// virt_region: &MemoryRegion, +// phys_region: &MemoryRegion, +// attr: AttributeFields, +// ) -> Result<(), &'static str> { +// // if platform::memory::mmu::virt_mmio_remap_region().overlaps(virt_region) { +// // return Err("Attempt to manually map into MMIO region"); +// // } +// // SAFETY: Make a mistake and you're dead! +// unsafe { +// kernel_map_at_unchecked(name, virt_region, phys_region, attr)?; +// } +// Ok(()) +// } + +/// MMIO remapping in the kernel translation tables. +/// +/// Typically used by device drivers. +/// +/// # Safety +/// +/// - Same as `kernel_map_at_unchecked()`, minus the aliasing part. +pub unsafe fn kernel_map_mmio( + _name: &'static str, + _mmio_descriptor: &MMIODescriptor, +) -> Result, &'static str> { + // let phys_region = MemoryRegion::from(*mmio_descriptor); + // let offset_into_start_page = mmio_descriptor.start_addr().offset_into_page(&4096); // FIXME: hardcoded page size + + // // Check if an identical region has been mapped for another driver. If so, reuse it. + // let virt_addr = if let Some(addr) = + // mapping_record::kernel_find_and_insert_mmio_duplicate(mmio_descriptor, name) + // { + // addr + // // Otherwise, allocate a new region and map it. + // } else { + // let Some(num_pages) = NonZeroUsize::new(phys_region.num_pages()) else { + // return Err("Requested 0 pages"); + // }; + + // let virt_region = + // page_alloc::kernel_mmio_va_allocator().lock(|allocator| allocator.alloc(num_pages))?; + + // // SAFETY: Make a mistake and you're dead, gaijin! + // unsafe { + // kernel_map_at_unchecked( + // name, + // &virt_region, + // &phys_region, + // AttributeFields { + // mem_attributes: MemAttributes::Device, + // acc_perms: AccessPermissions::ReadWrite, + // execute_never: true, + // }, + // )?; + // } + + // virt_region.start_addr() + // }; + + // Ok(virt_addr + offset_into_start_page) + Ok(Address::zero()) +} + +// Map a region in the kernel's translation tables. +// +// No input checks done, input is passed through to the architectural implementation (syscall?). +// +// # Safety +// +// - See `map_at()`. +// - Does not prevent aliasing. +// unsafe fn kernel_map_at_unchecked( +// name: &'static str, +// virt_region: &MemoryRegion, +// phys_region: &MemoryRegion, +// attr: AttributeFields, +// ) -> Result<(), &'static str> { +// crate::platform::memory::mmu::kernel_translation_tables().write(|tables| +// // SAFETY: Make a mistake and you're dead, gaijin! +// unsafe { tables.map_at(virt_region, phys_region, attr) })?; + +// if let Err(x) = mapping_record::kernel_add(name, virt_region, phys_region, attr) { +// // warn!("{x}"); +// return Err(x); +// } + +// Ok(()) +// } diff --git a/libs/mapping/src/mapping_attributes.rs b/libs/mapping/src/mapping_attributes.rs new file mode 100644 index 000000000..95cbf0e32 --- /dev/null +++ b/libs/mapping/src/mapping_attributes.rs @@ -0,0 +1,66 @@ +use core::fmt::{self, Formatter}; + +/// Architecture agnostic memory attributes. +#[derive(Copy, Clone, Debug, Eq, PartialOrd, PartialEq)] +pub enum MemAttributes { + /// Regular memory + CacheableDRAM, + /// Memory without caching + NonCacheableDRAM, + /// Device memory + Device, +} + +/// Architecture agnostic memory region access permissions. +#[derive(Copy, Clone, Debug, Eq, PartialOrd, PartialEq)] +pub enum AccessPermissions { + /// Read-only access + ReadOnly, + /// Read-write access + ReadWrite, +} + +/// Summary structure of memory region properties. +#[derive(Copy, Clone, Debug, Eq, PartialOrd, PartialEq)] +pub struct AttributeFields { + /// Attributes + pub mem_attributes: MemAttributes, + /// Permissions + pub acc_perms: AccessPermissions, + /// Disable executable code in this region + pub execute_never: bool, +} + +//-------------------------------------------------------------------------------------------------- +// Public Code +//-------------------------------------------------------------------------------------------------- + +impl Default for AttributeFields { + fn default() -> AttributeFields { + AttributeFields { + mem_attributes: MemAttributes::CacheableDRAM, + acc_perms: AccessPermissions::ReadWrite, + execute_never: true, + } + } +} + +/// Human-readable output of `AttributeFields` +impl fmt::Display for AttributeFields { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + let attr = match self.mem_attributes { + MemAttributes::CacheableDRAM => "C", + MemAttributes::NonCacheableDRAM => "NC", + MemAttributes::Device => "Dev", + }; + + let acc_p = match self.acc_perms { + AccessPermissions::ReadOnly => "RO", + AccessPermissions::ReadWrite => "RW", + }; + + let xn = if self.execute_never { "PXN" } else { "PX" }; + + write!(f, "{attr: <3} {acc_p} {xn: <3}") + } +} diff --git a/libs/memory/src/mapping_record.rs b/libs/mapping/src/mapping_record.rs similarity index 72% rename from libs/memory/src/mapping_record.rs rename to libs/mapping/src/mapping_record.rs index 1d3d342bb..18576cdfa 100644 --- a/libs/memory/src/mapping_record.rs +++ b/libs/mapping/src/mapping_record.rs @@ -5,13 +5,9 @@ //! A record of mapped pages. use { - super::{ - Address, Physical, Virtual, - types::{AccessPermissions, AttributeFields, MMIODescriptor, MemAttributes, MemoryRegion}, - }, - crate::platform, - liblocking::{self, InitStateLock}, - liblog::{info, warn}, + super::{AccessPermissions, AttributeFields, MMIODescriptor, MemAttributes, MemoryRegion}, + libaddress::{Address, Physical, Virtual}, + liblog::info, }; //-------------------------------------------------------------------------------------------------- @@ -19,9 +15,9 @@ use { //-------------------------------------------------------------------------------------------------- /// Type describing a virtual memory mapping. -#[allow(missing_docs)] +#[allow(missing_docs, dead_code)] #[derive(Copy, Clone)] -struct MappingRecordEntry { +struct MappingRecordEntry { pub users: [Option<&'static str>; 5], pub phys_start_addr: Address, pub virt_start_addr: Address, @@ -29,8 +25,9 @@ struct MappingRecordEntry { pub attribute_fields: AttributeFields, } -struct MappingRecord { - inner: [Option; 12], +#[allow(missing_docs, dead_code)] +struct MappingRecord { + inner: [Option>; 12], } //-------------------------------------------------------------------------------------------------- @@ -38,18 +35,19 @@ struct MappingRecord { //-------------------------------------------------------------------------------------------------- // FIXME: global state -static KERNEL_MAPPING_RECORD: InitStateLock = - InitStateLock::new(MappingRecord::new()); +// static KERNEL_MAPPING_RECORD: InitStateLock> = +// InitStateLock::new(MappingRecord::new()); //-------------------------------------------------------------------------------------------------- // Private Code //-------------------------------------------------------------------------------------------------- -impl MappingRecordEntry { +impl MappingRecordEntry { + #[allow(missing_docs, dead_code)] pub fn new( name: &'static str, - virt_region: &MemoryRegion, - phys_region: &MemoryRegion, + virt_region: &MemoryRegion, + phys_region: &MemoryRegion, attr: AttributeFields, ) -> Self { Self { @@ -61,6 +59,7 @@ impl MappingRecordEntry { } } + #[allow(missing_docs, dead_code)] fn find_next_free_user(&mut self) -> Result<&mut Option<&'static str>, &'static str> { if let Some(x) = self.users.iter_mut().find(|x| x.is_none()) { return Ok(x); @@ -69,6 +68,7 @@ impl MappingRecordEntry { Err("Storage for user info exhausted") } + #[allow(missing_docs, dead_code)] pub fn add_user(&mut self, user: &'static str) -> Result<(), &'static str> { let x = self.find_next_free_user()?; *x = Some(user); @@ -76,15 +76,18 @@ impl MappingRecordEntry { } } -impl MappingRecord { +impl MappingRecord { + #[allow(missing_docs, dead_code)] pub const fn new() -> Self { Self { inner: [None; 12] } } + #[allow(missing_docs, dead_code)] fn size(&self) -> usize { self.inner.iter().filter(|x| x.is_some()).count() } + #[allow(missing_docs, dead_code)] fn sort(&mut self) { let upper_bound_exclusive = self.size(); let entries = &mut self.inner.get_mut(0..upper_bound_exclusive).unwrap(); @@ -94,7 +97,10 @@ impl MappingRecord { } } - fn find_next_free(&mut self) -> Result<&mut Option, &'static str> { + #[allow(missing_docs, dead_code)] + fn find_next_free( + &mut self, + ) -> Result<&mut Option>, &'static str> { if let Some(x) = self.inner.iter_mut().find(|x| x.is_none()) { return Ok(x); } @@ -102,10 +108,11 @@ impl MappingRecord { Err("Storage for mapping info exhausted") } + #[allow(missing_docs, dead_code)] fn find_duplicate( &mut self, - phys_region: &MemoryRegion, - ) -> Option<&mut MappingRecordEntry> { + phys_region: &MemoryRegion, + ) -> Option<&mut MappingRecordEntry> { self.inner .iter_mut() .filter_map(|x| x.as_mut()) @@ -128,11 +135,12 @@ impl MappingRecord { /// # Returns /// /// Returns `Ok(())` on success, or a string error message on failure. + #[allow(missing_docs, dead_code)] pub fn add( &mut self, name: &'static str, - virt_region: &MemoryRegion, - phys_region: &MemoryRegion, + virt_region: &MemoryRegion, + phys_region: &MemoryRegion, attr: AttributeFields, ) -> Result<(), &'static str> { let x = self.find_next_free()?; @@ -149,6 +157,7 @@ impl MappingRecord { Ok(()) } + #[allow(missing_docs, dead_code)] pub fn print(&self) { info!( " -------------------------------------------------------------------------------------------------------------------------------------------" @@ -162,13 +171,13 @@ impl MappingRecord { ); for i in self.inner.iter().flatten() { - let size = i.num_pages * platform::KernelGranule::SIZE; + let size = i.num_pages * PAGE_SIZE; let virt_start = i.virt_start_addr; let virt_end_inclusive = virt_start + (size - 1); let phys_start = i.phys_start_addr; let phys_end_inclusive = phys_start + (size - 1); - let (size, unit) = crate::size_human_readable_ceil(size); + let (size, unit) = liblog::size_human_readable_ceil(size); let attr = match i.attribute_fields.mem_attributes { MemAttributes::CacheableDRAM => "C", @@ -219,36 +228,40 @@ impl MappingRecord { //-------------------------------------------------------------------------------------------------- // Public Code //-------------------------------------------------------------------------------------------------- -use liblocking::interface::ReadWriteEx; /// Add an entry to the mapping info record. -pub fn kernel_add( - name: &'static str, - virt_region: &MemoryRegion, - phys_region: &MemoryRegion, - attr: AttributeFields, +#[allow(missing_docs, dead_code)] +pub fn kernel_add( + _name: &'static str, + _virt_region: &MemoryRegion, + _phys_region: &MemoryRegion, + _attr: AttributeFields, ) -> Result<(), &'static str> { - KERNEL_MAPPING_RECORD.write(|mr| mr.add(name, virt_region, phys_region, attr)) + // KERNEL_MAPPING_RECORD.write(|mr| mr.add(name, virt_region, phys_region, attr)) + Ok(()) } -pub fn kernel_find_and_insert_mmio_duplicate( - mmio_descriptor: &MMIODescriptor, - new_user: &'static str, +#[allow(missing_docs, dead_code)] +pub fn kernel_find_and_insert_mmio_duplicate( + _mmio_descriptor: &MMIODescriptor, + _new_user: &'static str, ) -> Option> { - let phys_region: MemoryRegion = (*mmio_descriptor).into(); + // let phys_region: MemoryRegion = (*mmio_descriptor).into(); - KERNEL_MAPPING_RECORD.write(|mr| { - let dup = mr.find_duplicate(&phys_region)?; + // KERNEL_MAPPING_RECORD.write(|mr| { + // let dup = mr.find_duplicate(&phys_region)?; - if let Err(x) = dup.add_user(new_user) { - warn!("{x}"); - } + // if let Err(x) = dup.add_user(new_user) { + // warn!("{x}"); + // } - Some(dup.virt_start_addr) - }) + // Some(dup.virt_start_addr) + // }) + Some(Address::zero()) } /// Human-readable print of all recorded kernel mappings. +#[allow(missing_docs, dead_code)] pub fn kernel_print() { - KERNEL_MAPPING_RECORD.read(MappingRecord::print); + // KERNEL_MAPPING_RECORD.read(MappingRecord::print); } diff --git a/libs/mapping/src/memory_region.rs b/libs/mapping/src/memory_region.rs new file mode 100644 index 000000000..230ae3cc5 --- /dev/null +++ b/libs/mapping/src/memory_region.rs @@ -0,0 +1,140 @@ +use { + crate::{MMIODescriptor, PageAddress}, + core::{iter::Step, num::NonZeroUsize, ops::Range}, + libaddress::{Address, AddressType, Physical}, +}; + +/// A type that describes a region of memory in quantities of pages. +#[derive(Copy, Clone, Debug, Eq, PartialOrd, PartialEq)] +pub struct MemoryRegion { + start: PageAddress, + end_exclusive: PageAddress, +} + +impl MemoryRegion { + /// Create an instance. + pub fn new( + start: PageAddress, + end_exclusive: PageAddress, + ) -> Self { + assert!(start <= end_exclusive); + + Self { + start, + end_exclusive, + } + } + + fn as_range(&self) -> Range> { + self.into_iter() + } + + /// Returns the start page address. + pub fn start_page_addr(&self) -> PageAddress { + self.start + } + + /// Returns the start address. + pub fn start_addr(&self) -> Address { + self.start.into_inner() + } + + /// Returns the exclusive end page address. + pub fn end_exclusive_page_addr(&self) -> PageAddress { + self.end_exclusive + } + + /// Returns the exclusive end page address. + pub fn end_inclusive_page_addr(&self) -> PageAddress { + self.end_exclusive.checked_page_offset(-1).unwrap() + } + + /// Checks if self contains an address. + pub fn contains(&self, addr: Address) -> bool { + let page_addr = PageAddress::::from(addr.align_down_page(&PAGE_SIZE)); + self.as_range().contains(&page_addr) + } + + /// Checks if there is an overlap with another memory region. + pub fn overlaps(&self, other_region: &Self) -> bool { + let self_range = self.as_range(); + + self_range.contains(&other_region.start_page_addr()) + || self_range.contains(&other_region.end_inclusive_page_addr()) + } + + /// Returns the number of pages contained in this region. + pub fn num_pages(&self) -> usize { + PageAddress::steps_between(&self.start, &self.end_exclusive).0 + } + + /// Returns the size in bytes of this region. + pub fn size(&self) -> usize { + // Invariant: start <= end_exclusive, so do unchecked arithmetic. + let end_exclusive = self.end_exclusive.into_inner().as_usize(); + let start = self.start.into_inner().as_usize(); + + end_exclusive - start + } + + /// Splits the MemoryRegion like in the following diagram. + /// Left region is returned to the caller. Right region is the new region for this struct. + /// + /// -------------------------------------------------------------------------------- + /// | | | | | | | | | | | | | | | | | | | + /// -------------------------------------------------------------------------------- + /// ^ ^ ^ + /// | | | + /// `left_start` `left_end_exclusive` | + /// | + /// ^ | + /// | | + /// `right_start` `right_end_exclusive` + /// + pub fn take_first_n_pages(&mut self, num_pages: NonZeroUsize) -> Result { + let count: usize = num_pages.into(); + + let left_end_exclusive = self.start.checked_page_offset(count.cast_signed()); + let Some(left_end_exclusive) = left_end_exclusive else { + return Err("Overflow while calculating left_end_exclusive"); + }; + + if left_end_exclusive > self.end_exclusive { + return Err("Not enough free pages"); + } + + let allocation = Self { + start: self.start, + end_exclusive: left_end_exclusive, + }; + self.start = left_end_exclusive; + + Ok(allocation) + } +} + +impl IntoIterator + for MemoryRegion +{ + type Item = PageAddress; + type IntoIter = Range; + + fn into_iter(self) -> Self::IntoIter { + Range { + start: self.start, + end: self.end_exclusive, + } + } +} + +impl From for MemoryRegion { + fn from(desc: MMIODescriptor) -> Self { + let start = PageAddress::from(desc.start_addr().align_down_page(&PAGE_SIZE)); + let end_exclusive = PageAddress::from(desc.end_addr_exclusive().align_up_page(&PAGE_SIZE)); + + Self { + start, + end_exclusive, + } + } +} diff --git a/libs/mapping/src/page_address.rs b/libs/mapping/src/page_address.rs new file mode 100644 index 000000000..c605d5b3c --- /dev/null +++ b/libs/mapping/src/page_address.rs @@ -0,0 +1,86 @@ +use { + core::iter::Step, + libaddress::{Address, AddressType}, +}; + +/// A wrapper type around [Address] that ensures page alignment. +#[derive(Copy, Clone, Debug, Eq, PartialOrd, PartialEq)] +pub struct PageAddress { + inner: Address, +} + +impl PageAddress { + /// Unwraps the value. + pub fn into_inner(self) -> Address { + self.inner + } + + /// Calculates the offset from the page address. + /// + /// `count` is in units of [`PageAddress`]. For example, a count of 2 means `result = self + 2 * + /// page_size`. + pub fn checked_page_offset(self, count: isize) -> Option { + if count == 0 { + return Some(self); + } + + let delta = count.unsigned_abs().checked_mul(PAGE_SIZE)? as u64; + let result = if count.is_positive() { + self.inner.as_u64().checked_add(delta)? + } else { + self.inner.as_u64().checked_sub(delta)? + }; + + Some(Self { + inner: Address::::new(result), + }) + } +} + +impl From + for PageAddress +{ + fn from(addr: usize) -> Self { + assert!( + libaddress::align::is_aligned(addr as u64, PAGE_SIZE as u64), + "Input usize not page aligned" + ); + + Self { + inner: Address::::new(addr as u64), + } + } +} + +impl From> + for PageAddress +{ + fn from(addr: Address) -> Self { + assert!( + addr.is_page_aligned(&PAGE_SIZE), + "Input Address not page aligned" + ); + + Self { inner: addr } + } +} + +impl Step for PageAddress { + fn steps_between(start: &Self, end: &Self) -> (usize, Option) { + if start > end { + return (0, None); + } + + // Since start <= end, do unchecked arithmetic. + let steps = (end.inner.as_usize() - start.inner.as_usize()) / PAGE_SIZE; + (steps, Some(steps)) + } + + fn forward_checked(start: Self, count: usize) -> Option { + start.checked_page_offset(count.cast_signed()) + } + + fn backward_checked(start: Self, count: usize) -> Option { + start.checked_page_offset(-(count.cast_signed())) + } +} diff --git a/libs/memory/Cargo.toml b/libs/memory/Cargo.toml index 95ed34171..ffc752eed 100644 --- a/libs/memory/Cargo.toml +++ b/libs/memory/Cargo.toml @@ -27,6 +27,7 @@ cfg-if = { workspace = true } libaddress = { workspace = true } liblocking = { workspace = true } liblog = { workspace = true } +libmapping = { workspace = true } num = { workspace = true } once_cell = { workspace = true } snafu = { workspace = true } diff --git a/libs/memory/README.md b/libs/memory/README.md index b3f8824dd..a2a036965 100644 --- a/libs/memory/README.md +++ b/libs/memory/README.md @@ -14,6 +14,11 @@ This library should export the following: - Page-table hierarchy representation. This needs to be platform-independent. Abstract translation stages and page size granularity. + +---------------- work with MMU structures and kernel knowledge of mappings (THIS LIBRARY) + | +----- work with higher level mapping abstractions, without MMU details? + v v +libmmu -> libmapping + --- For more information please re-read. diff --git a/libs/memory/src/arch/aarch64/area_frame_allocator.rs b/libs/memory/src/arch/aarch64/area_frame_allocator.rs deleted file mode 100644 index 07a3bded4..000000000 --- a/libs/memory/src/arch/aarch64/area_frame_allocator.rs +++ /dev/null @@ -1,106 +0,0 @@ -/* - * SPDX-License-Identifier: BlueOak-1.0.0 - */ -use super::{Frame, FrameAllocator}; -use multiboot2::{MemoryArea, MemoryAreaIter}; // replace with DTB? - -pub struct AreaFrameAllocator { - next_free_frame: Frame, - current_area: Option<&'static MemoryArea>, - areas: MemoryAreaIter, - kernel_start: Frame, - kernel_end: Frame, - multiboot_start: Frame, - multiboot_end: Frame, -} - -impl FrameAllocator for AreaFrameAllocator { - fn allocate_frame(&mut self) -> Option { - if let Some(_area) = self.current_area { - // "Clone" the frame to return it if it's free. Frame doesn't - // implement Clone, but we can construct an identical frame. - let frame = Frame { - number: self.next_free_frame.number, - }; - - // the last frame of the current area - let current_area_last_frame = Frame::containing_address(0x3f00_0000); - // { - // let address = area.base_addr + area.length - 1; - // Frame::containing_address(address as usize) - // }; - - if frame > current_area_last_frame { - // all frames of current area are used, switch to next area - // self.choose_next_area(); - unimplemented!(); - } else if frame >= self.kernel_start && frame <= self.kernel_end { - // `frame` is used by the kernel - self.next_free_frame = Frame { - number: self.kernel_end.number + 1, - }; - } else if frame >= self.multiboot_start && frame <= self.multiboot_end { - // `frame` is used by the multiboot information structure - self.next_free_frame = Frame { - number: self.multiboot_end.number + 1, - }; - } else { - // frame is unused, increment `next_free_frame` and return it - self.next_free_frame.number += 1; - return Some(frame); - } - // `frame` was not valid, try it again with the updated `next_free_frame` - self.allocate_frame() - } else { - None // no free frames left - } - } - - fn deallocate_frame(&mut self, _frame: Frame) { - unimplemented!() - } -} - -// Fixme: no multiboot, but dtb instead with avail memory regions -// Need dtb parser here! - -impl AreaFrameAllocator { - pub fn new( - kernel_start: usize, - kernel_end: usize, - multiboot_start: usize, - multiboot_end: usize, - memory_areas: MemoryAreaIter, - ) -> AreaFrameAllocator { - let mut allocator = AreaFrameAllocator { - next_free_frame: Frame::containing_address(0), - current_area: None, - areas: memory_areas, - kernel_start: Frame::containing_address(kernel_start), - kernel_end: Frame::containing_address(kernel_end), - multiboot_start: Frame::containing_address(multiboot_start), - multiboot_end: Frame::containing_address(multiboot_end), - }; - // allocator.choose_next_area(); - allocator.next_free_frame = Frame::containing_address(0x100000); // start from 1Mb - allocator - } - - fn choose_next_area(&mut self) { - self.current_area = self - .areas - .clone() - .filter(|area| { - let address = area.base_addr + area.length - 1; - Frame::containing_address(address as usize) >= self.next_free_frame - }) - .min_by_key(|area| area.base_addr); - - if let Some(area) = self.current_area { - let start_frame = Frame::containing_address(area.base_addr as usize); - if self.next_free_frame < start_frame { - self.next_free_frame = start_frame; - } - } - } -} diff --git a/libs/memory/src/arch/aarch64/mmu/mod.rs b/libs/memory/src/arch/aarch64/mmu.rs similarity index 99% rename from libs/memory/src/arch/aarch64/mmu/mod.rs rename to libs/memory/src/arch/aarch64/mmu.rs index 37f8370b3..95a9099d8 100644 --- a/libs/memory/src/arch/aarch64/mmu/mod.rs +++ b/libs/memory/src/arch/aarch64/mmu.rs @@ -1,10 +1,11 @@ use { crate::{ - arch::mmu::translation_table::{ + AddressSpace, MMUEnableError, TranslationGranule, + arch::translation_table::{ PageFlags, PageSize, STAGE1_PAGE_DESCRIPTOR, STAGE1_TABLE_DESCRIPTOR, Size2MiB, Size4KiB, TableFlags, }, - mmu::{AddressSpace, AttributeFields, MMUEnableError, TranslationGranule, interface}, + interface, }, aarch64_cpu::{ asm::{self, barrier}, @@ -13,11 +14,10 @@ use { core::intrinsics::unlikely, libaddress::{Address, Physical}, liblog::println, + libmapping::AttributeFields, tock_registers::interfaces::{ReadWriteable, Readable, Writeable}, }; -pub(crate) mod translation_table; - //-------------------------------------------------------------------------------------------------- // Private Definitions //-------------------------------------------------------------------------------------------------- diff --git a/libs/memory/src/arch/aarch64/mod.rs b/libs/memory/src/arch/aarch64/mod.rs index 8c0e53ce7..a312c1844 100644 --- a/libs/memory/src/arch/aarch64/mod.rs +++ b/libs/memory/src/arch/aarch64/mod.rs @@ -9,14 +9,9 @@ pub mod features; // @todo make only pub re-export? pub mod mmu; mod page_size; mod phys_frame; +pub(crate) mod translation_table; mod virt_page; -// mod area_frame_allocator; -// pub use self::area_frame_allocator::AreaFrameAllocator; -// mod boot_allocator; // Hands out physical memory obtained from devtree -// use self::paging::PAGE_SIZE; - -// pub use crate::memory::{PhysAddr, VirtAddr}; pub use phys_frame::PhysFrame; /// @todo ?? diff --git a/libs/memory/src/arch/aarch64/mmu/translation_table.rs b/libs/memory/src/arch/aarch64/translation_table.rs similarity index 96% rename from libs/memory/src/arch/aarch64/mmu/translation_table.rs rename to libs/memory/src/arch/aarch64/translation_table.rs index ce1d709d3..761dad8b2 100644 --- a/libs/memory/src/arch/aarch64/mmu/translation_table.rs +++ b/libs/memory/src/arch/aarch64/translation_table.rs @@ -4,10 +4,10 @@ use core::{ }; use { - super::{Granule64KiB, Granule512MiB, mair}, - crate::{AccessPermissions, AttributeFields, MemAttributes, MemoryRegion, PageAddress}, + crate::arch_mmu::{Granule64KiB, Granule512MiB, mair}, core::convert, libaddress::{Address, PhysAddr, Physical, Virtual}, + libmapping::{AccessPermissions, AttributeFields, MemAttributes, MemoryRegion, PageAddress}, tock_registers::{ interfaces::{Readable, Writeable}, register_bitfields, @@ -107,12 +107,12 @@ pub struct TableDescriptor { value: u64, } -/// A page descriptor with 64 KiB aperture. +/// A page descriptor with given aperture. /// /// The output points to physical memory. #[derive(Copy, Clone)] #[repr(C)] -pub struct PageDescriptor { +pub struct PageDescriptor { value: u64, } @@ -185,7 +185,7 @@ impl TableDescriptor { } } -impl PageDescriptor { +impl PageDescriptor { /// Create an instance. /// /// Descriptor is invalid by default. @@ -195,12 +195,12 @@ impl PageDescriptor { /// Create an instance. pub fn from_output_page_addr( - phys_output_page_addr: PageAddress, + phys_output_page_addr: PageAddress, attribute_fields: AttributeFields, ) -> Self { let val = InMemoryRegister::::new(0); - let shifted = phys_output_page_addr.into_inner().as_usize() >> Granule64KiB::SHIFT; + let shifted = phys_output_page_addr.into_inner().as_usize() / PAGE_SIZE; // FIXME: Granule::SHIFT val.write( STAGE1_PAGE_DESCRIPTOR::OUTPUT_ADDR_64KiB.val(shifted as u64) + STAGE1_PAGE_DESCRIPTOR::AF::Accessed @@ -282,7 +282,7 @@ impl FixedSizeTranslationTable { /// Create an instance. #[allow(clippy::assertions_on_constants)] pub const fn new() -> Self { - assert!(libplatform::memory::KernelGranule::SIZE == Granule64KiB::SIZE); // assert! is const-fn-friendly + // assert!(libplatform::memory::KernelGranule::SIZE == Granule64KiB::SIZE); // assert! is const-fn-friendly // Can't have a zero-sized address space. assert!(NUM_TABLES > 0); @@ -447,11 +447,12 @@ impl crate::TranslationTable for FixedSizeTranslationTa return Err("Tried to map memory regions with different sizes"); } - if phys_region.end_exclusive_page_addr() - > crate::platform::memory::phys_addr_space_end_exclusive_addr() - { - return Err("Tried to map outside of physical address space"); - } + // TODO: keep track of phys memory end + // if phys_region.end_exclusive_page_addr() + // > crate::platform::memory::phys_addr_space_end_exclusive_addr() + // { + // return Err("Tried to map outside of physical address space"); + // } #[allow(clippy::useless_conversion)] let iter = phys_region.into_iter().zip(virt_region.into_iter()); diff --git a/libs/memory/src/lib.rs b/libs/memory/src/lib.rs index b0df57fad..846294400 100644 --- a/libs/memory/src/lib.rs +++ b/libs/memory/src/lib.rs @@ -26,48 +26,21 @@ #![feature(custom_test_frameworks)] use { - core::num::NonZeroUsize, - interface::MMU, - libaddress::{Address, Physical, Virtual}, - liblocking::interface::*, - liblog::warn, + libaddress::{Address, Physical}, snafu::Snafu, translation_table::interface::TranslationTable, }; -#[cfg(target_arch = "aarch64")] -use crate::arch::aarch64::mmu as arch_mmu; -use crate::platform::memory::KernelGranule; +pub use crate::arch::mmu as arch_mmu; mod arch; -mod mapping_record; pub mod page_alloc; pub mod translation_table; -mod types; - -pub use types::*; - -/// Convert a size into human readable format. -pub const fn size_human_readable_ceil(size: usize) -> (usize, &'static str) { - const KIB: usize = 1024; - const MIB: usize = 1024 * 1024; - const GIB: usize = 1024 * 1024 * 1024; - - if (size / GIB) > 0 { - (size.div_ceil(GIB), "GiB") - } else if (size / MIB) > 0 { - (size.div_ceil(MIB), "MiB") - } else if (size / KIB) > 0 { - (size.div_ceil(KIB), "KiB") - } else { - (size, "Byte") - } -} //-------------------------------------------------------------------------------------------------- // Architectural Public Reexports //-------------------------------------------------------------------------------------------------- -// pub use arch_mmu::mmu; +// pub use arch_mmu::*; //-------------------------------------------------------------------------------------------------- // Public Definitions @@ -132,31 +105,6 @@ pub trait AssociatedTranslationTable { // page_alloc::kernel_mmio_va_allocator().lock(|allocator| allocator.init(region)); // } -/// Map a region in the kernel's translation tables. -/// -/// No input checks done, input is passed through to the architectural implementation. -/// -/// # Safety -/// -/// - See `map_at()`. -/// - Does not prevent aliasing. -unsafe fn kernel_map_at_unchecked( - name: &'static str, - virt_region: &MemoryRegion, - phys_region: &MemoryRegion, - attr: AttributeFields, -) -> Result<(), &'static str> { - crate::platform::memory::mmu::kernel_translation_tables().write(|tables| - // SAFETY: Make a mistake and you're dead, gaijin! - unsafe { tables.map_at(virt_region, phys_region, attr) })?; - - if let Err(x) = mapping_record::kernel_add(name, virt_region, phys_region, attr) { - warn!("{x}"); - } - - Ok(()) -} - //-------------------------------------------------------------------------------------------------- // Public Code //-------------------------------------------------------------------------------------------------- @@ -189,128 +137,8 @@ impl AddressSpace { assert!(AS_SIZE.is_power_of_two()); // Check for architectural restrictions as well. - Self::arch_address_space_size_sanity_checks(); + // Self::arch_address_space_size_sanity_checks(); AS_SIZE } } - -//-------------------------------------------------------------------------------------------------- -// Public Code -// FIXME: this code should move to init_thread and be only used during boot-up! (see paging.rs) -//-------------------------------------------------------------------------------------------------- - -/// Raw mapping of a virtual to physical region in the kernel translation tables. -/// -/// Prevents mapping into the MMIO range of the tables. -/// -/// # Safety -/// -/// - See `kernel_map_at_unchecked()`. -/// - Does not prevent aliasing. Currently, the callers must be trusted. -pub unsafe fn kernel_map_at( - name: &'static str, - virt_region: &MemoryRegion, - phys_region: &MemoryRegion, - attr: AttributeFields, -) -> Result<(), &'static str> { - if platform::memory::mmu::virt_mmio_remap_region().overlaps(virt_region) { - return Err("Attempt to manually map into MMIO region"); - } - - // SAFETY: Make a mistake and you're dead, gaijin! - unsafe { - kernel_map_at_unchecked(name, virt_region, phys_region, attr)?; - } - - Ok(()) -} - -/// MMIO remapping in the kernel translation tables. -/// -/// Typically used by device drivers. -/// -/// # Safety -/// -/// - Same as `kernel_map_at_unchecked()`, minus the aliasing part. -pub unsafe fn kernel_map_mmio( - name: &'static str, - mmio_descriptor: &MMIODescriptor, -) -> Result, &'static str> { - let phys_region = MemoryRegion::from(*mmio_descriptor); - let offset_into_start_page = mmio_descriptor - .start_addr() - .offset_into_page(&KernelGranule::SIZE); // FIXME: fixed page size - - // Check if an identical region has been mapped for another driver. If so, reuse it. - let virt_addr = if let Some(addr) = - mapping_record::kernel_find_and_insert_mmio_duplicate(mmio_descriptor, name) - { - addr - // Otherwise, allocate a new region and map it. - } else { - let Some(num_pages) = NonZeroUsize::new(phys_region.num_pages()) else { - return Err("Requested 0 pages"); - }; - - let virt_region = - page_alloc::kernel_mmio_va_allocator().lock(|allocator| allocator.alloc(num_pages))?; - - // SAFETY: Make a mistake and you're dead, gaijin! - unsafe { - kernel_map_at_unchecked( - name, - &virt_region, - &phys_region, - AttributeFields { - mem_attributes: MemAttributes::Device, - acc_perms: AccessPermissions::ReadWrite, - execute_never: true, - }, - )?; - } - - virt_region.start_addr() - }; - - Ok(virt_addr + offset_into_start_page) -} - -/// Map the kernel's binary. Returns the translation table's base address. -/// -/// # Safety -/// -/// - See [`bsp::memory::mmu::kernel_map_binary()`]. -pub unsafe fn kernel_map_binary() -> Result, &'static str> { - let phys_kernel_tables_base_addr = - platform::memory::mmu::kernel_translation_tables().write(|tables| { - tables.init().unwrap(); - tables.phys_base_address() - }); - - // SAFETY: Make a mistake and you're dead, gaijin! - unsafe { - platform::memory::mmu::kernel_map_binary()?; - } - - Ok(phys_kernel_tables_base_addr) -} - -/// Enable the MMU and data + instruction caching. -/// -/// # Safety -/// -/// - Crucial function during kernel init. Changes the the complete memory view of the processor. -#[inline] -pub unsafe fn enable_mmu_and_caching( - phys_tables_base_addr: Address, -) -> Result<(), MMUEnableError> { - // SAFETY: Make a mistake and you're dead, gaijin! - unsafe { arch_mmu::mmu().enable_mmu_and_caching(phys_tables_base_addr) } -} - -/// Human-readable print of all recorded kernel mappings. -#[inline] -pub fn kernel_print_mappings() { - mapping_record::kernel_print(); -} diff --git a/libs/memory/src/page_alloc.rs b/libs/memory/src/page_alloc.rs index 1aa6991ca..2fe35bb06 100644 --- a/libs/memory/src/page_alloc.rs +++ b/libs/memory/src/page_alloc.rs @@ -25,8 +25,9 @@ pub struct PageAllocator { // Global instances //-------------------------------------------------------------------------------------------------- +// TODO: drop this, kernel should not be allocating any memory!! static KERNEL_MMIO_VA_ALLOCATOR: IRQSafeNullLock> = - IRQSafeNullLock::new(PageAllocator::new()); + IRQSafeNullLock::new(PageAllocator::::new()); //-------------------------------------------------------------------------------------------------- // Public Code diff --git a/libs/memory/src/translation_table.rs b/libs/memory/src/translation_table.rs index 2e6461850..07c10db6a 100644 --- a/libs/memory/src/translation_table.rs +++ b/libs/memory/src/translation_table.rs @@ -1,17 +1,16 @@ -//! Translation table. +//! Translation table interface. +// TODO: Move to libmmu, or libmapping - higher level mapping interface. -#[cfg(target_arch = "aarch64")] -use crate::arch::aarch64::mmu::translation_table as arch_translation_table; +use crate::arch::translation_table as arch_translation_table; use { - super::{AttributeFields, MemoryRegion}, libaddress::{Address, Physical, Virtual}, + libmapping::{AttributeFields, MemoryRegion}, }; //-------------------------------------------------------------------------------------------------- // Architectural Public Reexports //-------------------------------------------------------------------------------------------------- -#[cfg(target_arch = "aarch64")] pub use arch_translation_table::FixedSizeTranslationTable; //-------------------------------------------------------------------------------------------------- diff --git a/libs/memory/src/types.rs b/libs/memory/src/types.rs deleted file mode 100644 index beafaa4d1..000000000 --- a/libs/memory/src/types.rs +++ /dev/null @@ -1,334 +0,0 @@ -//-------------------------------------------------------------------------------------------------- -// Public Definitions -//-------------------------------------------------------------------------------------------------- - -use { - crate::platform::KernelGranule, - core::{ - fmt::{self, Formatter}, - iter::Step, - num::NonZeroUsize, - ops::Range, - }, - libaddress::{Address, AddressType, Physical}, -}; - -/// A wrapper type around [Address] that ensures page alignment. -#[derive(Copy, Clone, Debug, Eq, PartialOrd, PartialEq)] -pub struct PageAddress { - inner: Address, -} - -/// A type that describes a region of memory in quantities of pages. -#[derive(Copy, Clone, Debug, Eq, PartialOrd, PartialEq)] -pub struct MemoryRegion { - start: PageAddress, - end_exclusive: PageAddress, -} - -/// Architecture agnostic memory attributes. -#[derive(Copy, Clone, Debug, Eq, PartialOrd, PartialEq)] -pub enum MemAttributes { - /// Regular memory - CacheableDRAM, - /// Memory without caching - NonCacheableDRAM, - /// Device memory - Device, -} - -/// Architecture agnostic memory region access permissions. -#[derive(Copy, Clone, Debug, Eq, PartialOrd, PartialEq)] -pub enum AccessPermissions { - /// Read-only access - ReadOnly, - /// Read-write access - ReadWrite, -} - -/// Summary structure of memory region properties. -#[derive(Copy, Clone, Debug, Eq, PartialOrd, PartialEq)] -pub struct AttributeFields { - /// Attributes - pub mem_attributes: MemAttributes, - /// Permissions - pub acc_perms: AccessPermissions, - /// Disable executable code in this region - pub execute_never: bool, -} - -/// An MMIO descriptor for use in device drivers. -#[derive(Copy, Clone)] -pub struct MMIODescriptor { - start_addr: Address, - end_addr_exclusive: Address, -} - -//-------------------------------------------------------------------------------------------------- -// Public Code -//-------------------------------------------------------------------------------------------------- - -//------------------------------------------------------------------------------ -// PageAddress -//------------------------------------------------------------------------------ -impl PageAddress { - /// Unwraps the value. - pub fn into_inner(self) -> Address { - self.inner - } - - /// Calculates the offset from the page address. - /// - /// `count` is in units of [`PageAddress`]. For example, a count of 2 means `result = self + 2 * - /// page_size`. - pub fn checked_page_offset(self, count: isize) -> Option { - if count == 0 { - return Some(self); - } - - let delta = count.unsigned_abs().checked_mul(KernelGranule::SIZE)? as u64; - let result = if count.is_positive() { - self.inner.as_u64().checked_add(delta)? - } else { - self.inner.as_u64().checked_sub(delta)? - }; - - Some(Self { - inner: Address::::new(result), - }) - } -} - -impl From for PageAddress { - fn from(addr: usize) -> Self { - assert!( - libaddress::align::is_aligned(addr as u64, KernelGranule::SIZE as u64), - "Input usize not page aligned" - ); - - Self { - inner: Address::::new(addr as u64), - } - } -} - -impl From> for PageAddress { - fn from(addr: Address) -> Self { - assert!( - addr.is_page_aligned(&KernelGranule::SIZE), // FIXME: fixed page size - "Input Address not page aligned" - ); - - Self { inner: addr } - } -} - -impl Step for PageAddress { - fn steps_between(start: &Self, end: &Self) -> (usize, Option) { - if start > end { - return (0, None); - } - - // Since start <= end, do unchecked arithmetic. - let steps = (end.inner.as_usize() - start.inner.as_usize()) >> KernelGranule::SHIFT; - (steps, Some(steps)) - } - - fn forward_checked(start: Self, count: usize) -> Option { - start.checked_page_offset(count.cast_signed()) - } - - fn backward_checked(start: Self, count: usize) -> Option { - start.checked_page_offset(-(count.cast_signed())) - } -} - -//------------------------------------------------------------------------------ -// MemoryRegion -//------------------------------------------------------------------------------ -impl MemoryRegion { - /// Create an instance. - pub fn new(start: PageAddress, end_exclusive: PageAddress) -> Self { - assert!(start <= end_exclusive); - - Self { - start, - end_exclusive, - } - } - - fn as_range(&self) -> Range> { - self.into_iter() - } - - /// Returns the start page address. - pub fn start_page_addr(&self) -> PageAddress { - self.start - } - - /// Returns the start address. - pub fn start_addr(&self) -> Address { - self.start.into_inner() - } - - /// Returns the exclusive end page address. - pub fn end_exclusive_page_addr(&self) -> PageAddress { - self.end_exclusive - } - - /// Returns the exclusive end page address. - pub fn end_inclusive_page_addr(&self) -> PageAddress { - self.end_exclusive.checked_page_offset(-1).unwrap() - } - - /// Checks if self contains an address. - pub fn contains(&self, addr: Address) -> bool { - let page_addr = PageAddress::from(addr.align_down_page(&KernelGranule::SIZE)); // FIXME: fixed page size - self.as_range().contains(&page_addr) - } - - /// Checks if there is an overlap with another memory region. - pub fn overlaps(&self, other_region: &Self) -> bool { - let self_range = self.as_range(); - - self_range.contains(&other_region.start_page_addr()) - || self_range.contains(&other_region.end_inclusive_page_addr()) - } - - /// Returns the number of pages contained in this region. - pub fn num_pages(&self) -> usize { - PageAddress::steps_between(&self.start, &self.end_exclusive).0 - } - - /// Returns the size in bytes of this region. - pub fn size(&self) -> usize { - // Invariant: start <= end_exclusive, so do unchecked arithmetic. - let end_exclusive = self.end_exclusive.into_inner().as_usize(); - let start = self.start.into_inner().as_usize(); - - end_exclusive - start - } - - /// Splits the MemoryRegion like in the following diagram. - /// Left region is returned to the caller. Right region is the new region for this struct. - /// - /// -------------------------------------------------------------------------------- - /// | | | | | | | | | | | | | | | | | | | - /// -------------------------------------------------------------------------------- - /// ^ ^ ^ - /// | | | - /// `left_start` `left_end_exclusive` | - /// | - /// ^ | - /// | | - /// `right_start` `right_end_exclusive` - /// - pub fn take_first_n_pages(&mut self, num_pages: NonZeroUsize) -> Result { - let count: usize = num_pages.into(); - - let left_end_exclusive = self.start.checked_page_offset(count.cast_signed()); - let Some(left_end_exclusive) = left_end_exclusive else { - return Err("Overflow while calculating left_end_exclusive"); - }; - - if left_end_exclusive > self.end_exclusive { - return Err("Not enough free pages"); - } - - let allocation = Self { - start: self.start, - end_exclusive: left_end_exclusive, - }; - self.start = left_end_exclusive; - - Ok(allocation) - } -} - -impl IntoIterator for MemoryRegion { - type Item = PageAddress; - type IntoIter = Range; - - fn into_iter(self) -> Self::IntoIter { - Range { - start: self.start, - end: self.end_exclusive, - } - } -} - -impl From for MemoryRegion { - fn from(desc: MMIODescriptor) -> Self { - let start = PageAddress::from(desc.start_addr.align_down_page(&KernelGranule::SIZE)); // FIXME: fixed page size - let end_exclusive = PageAddress::from( - desc.end_addr_exclusive() - .align_up_page(&KernelGranule::SIZE), // FIXME: fixed page size - ); - - Self { - start, - end_exclusive, - } - } -} - -//------------------------------------------------------------------------------ -// MMIODescriptor -//------------------------------------------------------------------------------ - -impl MMIODescriptor { - /// Create an instance. - pub const fn new(start_addr: Address, size: usize) -> Self { - assert!(size > 0); - let end_addr_exclusive = Address::new(start_addr.as_u64() + size as u64); - - Self { - start_addr, - end_addr_exclusive, - } - } - - /// Return the start address. - pub const fn start_addr(&self) -> Address { - self.start_addr - } - - /// Return the exclusive end address. - pub fn end_addr_exclusive(&self) -> Address { - self.end_addr_exclusive - } -} - -//------------------------------------------------------------------------------ -// AttributeFields -//------------------------------------------------------------------------------ - -impl Default for AttributeFields { - fn default() -> AttributeFields { - AttributeFields { - mem_attributes: MemAttributes::CacheableDRAM, - acc_perms: AccessPermissions::ReadWrite, - execute_never: true, - } - } -} - -/// Human-readable output of `AttributeFields` -impl fmt::Display for AttributeFields { - fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { - let attr = match self.mem_attributes { - MemAttributes::CacheableDRAM => "C", - MemAttributes::NonCacheableDRAM => "NC", - MemAttributes::Device => "Dev", - }; - - let acc_p = match self.acc_perms { - AccessPermissions::ReadOnly => "RO", - AccessPermissions::ReadWrite => "RW", - }; - - let xn = if self.execute_never { "PXN" } else { "PX" }; - - write!(f, "{attr: <3} {acc_p} {xn: <3}") - } -} diff --git a/libs/mmio/Cargo.toml b/libs/mmio/Cargo.toml new file mode 100644 index 000000000..18a6e73f1 --- /dev/null +++ b/libs/mmio/Cargo.toml @@ -0,0 +1,26 @@ +[package] +name = "libmmio" +description = "Vesper nanokernel MMIO abstractions." +authors = { workspace = true } +categories = { workspace = true } +documentation = { workspace = true } +edition = { workspace = true } +homepage = { workspace = true } +license = { workspace = true } +readme = { workspace = true } +repository = { workspace = true } +version = { workspace = true } +publish = false + +[badges] +maintenance = { status = "experimental" } + +[dependencies] +safe-mmio = { workspace = true } + +# Tests are offloaded to kernel integration tests binary. +[lib] +test = false + +[lints] +workspace = true diff --git a/libs/mmio/README.md b/libs/mmio/README.md new file mode 100644 index 000000000..668aec85d --- /dev/null +++ b/libs/mmio/README.md @@ -0,0 +1,3 @@ +A safe wrapper for MMIO device access. + +Built on top of safe_mmio. diff --git a/libs/platform/src/raspberrypi/device_driver/common.rs b/libs/mmio/src/lib.rs similarity index 94% rename from libs/platform/src/raspberrypi/device_driver/common.rs rename to libs/mmio/src/lib.rs index 1f72e776a..71031b499 100644 --- a/libs/platform/src/raspberrypi/device_driver/common.rs +++ b/libs/mmio/src/lib.rs @@ -2,8 +2,7 @@ // // Copyright (c) 2020-2022 Andre Richter -//! Common device driver code. -// @todo: Move to libprimitive or libdriver or sth? +#![no_std] use { core::{marker::PhantomData, ops}, @@ -14,6 +13,9 @@ use { // Public Definitions //-------------------------------------------------------------------------------------------------- +pub mod mmio_descriptor; +pub use mmio_descriptor::*; + #[allow(clippy::partial_pub_fields)] pub struct MMIODerefWrapper { pub base_addr: Address, // @todo unmake public, GPIO::Pin uses it diff --git a/libs/mmio/src/mmio_descriptor.rs b/libs/mmio/src/mmio_descriptor.rs new file mode 100644 index 000000000..12d1fb22e --- /dev/null +++ b/libs/mmio/src/mmio_descriptor.rs @@ -0,0 +1,31 @@ +use libaddress::{Address, Physical}; + +/// An MMIO descriptor for use in device drivers. +#[derive(Copy, Clone)] +pub struct MMIODescriptor { + start_addr: Address, + end_addr_exclusive: Address, +} + +impl MMIODescriptor { + /// Create an instance. + pub const fn new(start_addr: Address, size: usize) -> Self { + assert!(size > 0); + let end_addr_exclusive = Address::new(start_addr.as_u64() + size as u64); + + Self { + start_addr, + end_addr_exclusive, + } + } + + /// Return the start address. + pub const fn start_addr(&self) -> Address { + self.start_addr + } + + /// Return the exclusive end address. + pub fn end_addr_exclusive(&self) -> Address { + self.end_addr_exclusive + } +} diff --git a/libs/platform/Cargo.toml b/libs/platform/Cargo.toml index 577591681..448aeaf04 100644 --- a/libs/platform/Cargo.toml +++ b/libs/platform/Cargo.toml @@ -35,13 +35,13 @@ cfg-if = { workspace = true } libaddress = { workspace = true } libconsole = { workspace = true } libcpu = { workspace = true } +libdriver = { workspace = true } libexception = { workspace = true } libkernel-state = { workspace = true } liblocal-irq = { workspace = true } liblocking = { workspace = true } liblog = { workspace = true } -libmemory = { workspace = true } -libdriver = { workspace = true } +libmapping = { workspace = true } libprimitives = { workspace = true } libtime = { workspace = true } once_cell = { workspace = true } diff --git a/libs/platform/src/raspberrypi/device_driver/arm/gicv2/gicc.rs b/libs/platform/src/raspberrypi/device_driver/arm/gicv2/gicc.rs index 61eee7700..13d4ada99 100644 --- a/libs/platform/src/raspberrypi/device_driver/arm/gicv2/gicc.rs +++ b/libs/platform/src/raspberrypi/device_driver/arm/gicv2/gicc.rs @@ -6,7 +6,7 @@ use { crate::platform::device_driver::common::MMIODerefWrapper, - libmemory::{Address, Virtual}, + libaddress::{Address, Virtual}, tock_registers::{ interfaces::{Readable, Writeable}, register_bitfields, register_structs, diff --git a/libs/platform/src/raspberrypi/device_driver/arm/gicv2/gicd.rs b/libs/platform/src/raspberrypi/device_driver/arm/gicv2/gicd.rs index 7d2b73ab6..3a732c129 100644 --- a/libs/platform/src/raspberrypi/device_driver/arm/gicv2/gicd.rs +++ b/libs/platform/src/raspberrypi/device_driver/arm/gicv2/gicd.rs @@ -9,8 +9,8 @@ use { crate::platform::device_driver::common::MMIODerefWrapper, + libaddress::{Address, Virtual}, liblocking::IRQSafeNullLock, - libmemory::{Address, Virtual}, tock_registers::{ interfaces::{Readable, Writeable}, register_bitfields, register_structs, diff --git a/libs/platform/src/raspberrypi/device_driver/arm/gicv2/mod.rs b/libs/platform/src/raspberrypi/device_driver/arm/gicv2/mod.rs index 5796ee5d6..c828588b9 100644 --- a/libs/platform/src/raspberrypi/device_driver/arm/gicv2/mod.rs +++ b/libs/platform/src/raspberrypi/device_driver/arm/gicv2/mod.rs @@ -81,11 +81,11 @@ mod gicd; use { crate::platform::cpu::BOOT_CORE_ID, + libaddress::{Address, Virtual}, libexception::exception::asynchronous::{ IRQContext, IRQHandlerDescriptor, interface::IRQManager, }, liblocking::{self, InitStateLock}, - libmemory::{Address, Virtual}, libprimitives::BoundedUsize, }; diff --git a/libs/platform/src/raspberrypi/device_driver/bcm/mailbox.rs b/libs/platform/src/raspberrypi/device_driver/bcm/mailbox.rs index 9c8a0485a..6f48521ca 100644 --- a/libs/platform/src/raspberrypi/device_driver/bcm/mailbox.rs +++ b/libs/platform/src/raspberrypi/device_driver/bcm/mailbox.rs @@ -22,7 +22,7 @@ use { sync::atomic::{Ordering, compiler_fence}, }, // liblocking::IRQSafeNullLock, - // libmemory::{Address, Virtual}, + // libaddress::{Address, Virtual}, snafu::Snafu, tock_registers::{ interfaces::{Readable, Writeable}, diff --git a/libs/platform/src/raspberrypi/device_driver/mod.rs b/libs/platform/src/raspberrypi/device_driver/mod.rs index 27fd6432d..a1b76e8d9 100644 --- a/libs/platform/src/raspberrypi/device_driver/mod.rs +++ b/libs/platform/src/raspberrypi/device_driver/mod.rs @@ -9,8 +9,6 @@ pub mod arm; #[cfg(any(board_rpi3, board_rpi4))] pub mod bcm; -pub mod common; - #[cfg(board_rpi4)] pub use arm::*; #[cfg(any(board_rpi3, board_rpi4))] diff --git a/libs/platform/src/raspberrypi/drivers.rs b/libs/platform/src/raspberrypi/drivers.rs index 129386571..5cfae0cd9 100644 --- a/libs/platform/src/raspberrypi/drivers.rs +++ b/libs/platform/src/raspberrypi/drivers.rs @@ -6,7 +6,7 @@ use { sync::atomic::{AtomicBool, Ordering}, }, libdriver::drivers::DriverManager, - libmemory::{mmu::MMIODescriptor, platform::memory::map::mmio}, + libmmio::MMIODescriptor, }; //-------------------------------------------------------------------------------------------------- @@ -94,7 +94,7 @@ unsafe fn instantiate_uart() -> Result<(), &'static str> { let mmio_descriptor = MMIODescriptor::new(mmio::PL011_UART_BASE, mmio::PL011_UART_SIZE); // SAFETY: You may believe praying will save you. let virt_addr = unsafe { - libmemory::mmu::kernel_map_mmio(device_driver::PL011Uart::COMPATIBLE, &mmio_descriptor)? + libmapping::kernel_map_mmio(device_driver::PL011Uart::COMPATIBLE, &mmio_descriptor)? }; #[allow(static_mut_refs)] @@ -122,9 +122,8 @@ unsafe fn post_init_pl011_uart() -> Result<(), &'static str> { unsafe fn instantiate_gpio() -> Result<(), &'static str> { let mmio_descriptor = MMIODescriptor::new(mmio::GPIO_BASE, mmio::GPIO_SIZE); // SAFETY: You may believe praying will save you. - let virt_addr = unsafe { - libmemory::mmu::kernel_map_mmio(device_driver::GPIO::COMPATIBLE, &mmio_descriptor)? - }; + let virt_addr = + unsafe { libmapping::kernel_map_mmio(device_driver::GPIO::COMPATIBLE, &mmio_descriptor)? }; #[allow(static_mut_refs)] // SAFETY: You may believe praying will save you. @@ -153,7 +152,7 @@ unsafe fn instantiate_interrupt_controller() -> Result<(), &'static str> { MMIODescriptor::new(mmio::PERIPHERAL_IC_BASE, mmio::PERIPHERAL_IC_SIZE); // SAFETY: You may believe praying will save you. let periph_virt_addr = unsafe { - libmemory::mmu::kernel_map_mmio( + libmapping::kernel_map_mmio( device_driver::InterruptController::COMPATIBLE, &periph_mmio_descriptor, )? diff --git a/userspace/README.md b/userspace/README.md new file mode 100644 index 000000000..0b69010e1 --- /dev/null +++ b/userspace/README.md @@ -0,0 +1 @@ +This should move outside of vesper repo completely, as well as many other userspace drivers. diff --git a/userspace/shell.rs b/userspace/shell.rs index f9b7c0e0c..0b5e6508d 100644 --- a/userspace/shell.rs +++ b/userspace/shell.rs @@ -1,6 +1,6 @@ fn print_mmu_state_and_features() { // use machine::memory::mmu::interface::MMU; - libmemory::arch::features::print_features(); + features::print_mmu_features(); } // TODO: AFTER INIT_THREAD, one of the userspace processes From 62c97e1fa48c143bde737f25dfe7401bdd26f101 Mon Sep 17 00:00:00 2001 From: Berkus Decker Date: Mon, 9 Feb 2026 01:48:52 +0200 Subject: [PATCH 071/107] wip: Claude's idea for how libmemory could look like, lets test --- kernel/init_thread/Cargo.toml | 2 +- kernel/nucleus/Cargo.toml | 2 +- kernel/nucleus/build.rs | 2 +- kernel/nucleus/src/main.rs | 2 +- libs/boot/Cargo.toml | 1 - libs/boot/src/arch/aarch64/boot.rs | 2 +- libs/machine/Cargo.toml | 2 +- libs/memory/Cargo.toml | 9 - libs/memory/src/arch/aarch64/mmu.rs | 520 ++---------- libs/memory/src/arch/aarch64/mmu2.rs | 631 --------------- .../src/arch/aarch64/mmu_experimental.rs | 279 ------- libs/memory/src/arch/aarch64/mod.rs | 25 +- libs/memory/src/arch/aarch64/page_size.rs | 68 -- libs/memory/src/arch/aarch64/phys_frame.rs | 200 ----- libs/memory/src/arch/aarch64/translation.rs | 276 +++++++ .../src/arch/aarch64/translation_table.rs | 748 ------------------ libs/memory/src/arch/aarch64/virt_page.rs | 334 -------- libs/memory/src/arch/mod.rs | 3 - libs/memory/src/arch_trait.rs | 72 ++ libs/memory/src/error.rs | 27 + libs/memory/src/lib.rs | 155 +--- libs/memory/src/page_alloc.rs | 77 -- libs/memory/src/table.rs | 240 ++++++ libs/memory/src/translation_table.rs | 51 -- libs/memory/src/walk.rs | 63 ++ libs/mmio/Cargo.toml | 1 + libs/object/Cargo.toml | 2 +- libs/platform/Cargo.toml | 1 + .../src/raspberrypi/device_driver/bcm/gpio.rs | 3 +- .../bcm/interrupt_controller/peripheral_ic.rs | 2 +- .../raspberrypi/device_driver/bcm/mailbox.rs | 5 +- .../device_driver/bcm/mini_uart.rs | 7 +- .../device_driver/bcm/pl011_uart.rs | 3 +- .../raspberrypi/device_driver/bcm/power.rs | 2 +- libs/platform/src/raspberrypi/drivers.rs | 6 +- libs/platform/src/raspberrypi/memory.rs | 21 +- play/memtab.rs | 520 ------------ play/memtab2.rs | 520 ------------ 38 files changed, 818 insertions(+), 4066 deletions(-) delete mode 100644 libs/memory/src/arch/aarch64/mmu2.rs delete mode 100644 libs/memory/src/arch/aarch64/mmu_experimental.rs delete mode 100644 libs/memory/src/arch/aarch64/page_size.rs delete mode 100644 libs/memory/src/arch/aarch64/phys_frame.rs create mode 100644 libs/memory/src/arch/aarch64/translation.rs delete mode 100644 libs/memory/src/arch/aarch64/translation_table.rs delete mode 100644 libs/memory/src/arch/aarch64/virt_page.rs create mode 100644 libs/memory/src/arch_trait.rs create mode 100644 libs/memory/src/error.rs delete mode 100644 libs/memory/src/page_alloc.rs create mode 100644 libs/memory/src/table.rs delete mode 100644 libs/memory/src/translation_table.rs create mode 100644 libs/memory/src/walk.rs delete mode 100644 play/memtab.rs delete mode 100644 play/memtab2.rs diff --git a/kernel/init_thread/Cargo.toml b/kernel/init_thread/Cargo.toml index 8ee91a723..55dabacd8 100644 --- a/kernel/init_thread/Cargo.toml +++ b/kernel/init_thread/Cargo.toml @@ -41,7 +41,7 @@ libkernel-state = { workspace = true } liblog = { workspace = true } liblocking = { workspace = true } libmachine = { workspace = true } -libmemory = { workspace = true } +# libmemory = { workspace = true } libplatform = { workspace = true } libprint = { workspace = true } libqemu = { workspace = true, optional = true } diff --git a/kernel/nucleus/Cargo.toml b/kernel/nucleus/Cargo.toml index 2a0ee5e82..5444e204b 100644 --- a/kernel/nucleus/Cargo.toml +++ b/kernel/nucleus/Cargo.toml @@ -43,7 +43,7 @@ libkernel-state = { workspace = true } liblocking = { workspace = true } liblog = { workspace = true } libmachine = { workspace = true } -libmemory = { workspace = true } +libmapping = { workspace = true } libobject = { workspace = true } libplatform = { workspace = true } libprint = { workspace = true } diff --git a/kernel/nucleus/build.rs b/kernel/nucleus/build.rs index 17f721b29..5030da575 100644 --- a/kernel/nucleus/build.rs +++ b/kernel/nucleus/build.rs @@ -1,6 +1,6 @@ //! This build script is used to link main kernel binary. -const LINKER_SCRIPT: &str = "libs/platform/src/platform/raspberrypi/linker/nucleus.ld"; +const LINKER_SCRIPT: &str = "libs/platform/src/raspberrypi/linker/nucleus.ld"; const LINKER_SCRIPT_AUX: &str = "libs/exception/src/arch/aarch64/linker/aarch64-exceptions.ld"; fn main() { diff --git a/kernel/nucleus/src/main.rs b/kernel/nucleus/src/main.rs index 44bbd5a83..4402b5722 100644 --- a/kernel/nucleus/src/main.rs +++ b/kernel/nucleus/src/main.rs @@ -35,7 +35,7 @@ use { libexception::arch::aarch64::ExceptionContext, liblocking::{IRQSafeNullLock, interface::Mutex}, liblog::{info, println, warn}, - libmemory::mmu::AccessPermissions, + libmapping::AccessPermissions, libobject::{ArchType, CapError, KeySlot}, libqemu::{semi_print, semi_println}, }; diff --git a/libs/boot/Cargo.toml b/libs/boot/Cargo.toml index 1955df528..50c764f88 100644 --- a/libs/boot/Cargo.toml +++ b/libs/boot/Cargo.toml @@ -23,7 +23,6 @@ aarch64-cpu = { workspace = true } libcpu = { workspace = true } libplatform = { workspace = true } tock-registers = { workspace = true } -# probably libmemory [lints] workspace = true diff --git a/libs/boot/src/arch/aarch64/boot.rs b/libs/boot/src/arch/aarch64/boot.rs index c675b21c1..754075cf5 100644 --- a/libs/boot/src/arch/aarch64/boot.rs +++ b/libs/boot/src/arch/aarch64/boot.rs @@ -15,7 +15,7 @@ use { aarch64_cpu::registers::*, core::arch::global_asm, libcpu::endless_sleep, - libplatform::platform::cpu::BOOT_CORE_ID, + libplatform::cpu::BOOT_CORE_ID, tock_registers::interfaces::{Readable, Writeable}, }; diff --git a/libs/machine/Cargo.toml b/libs/machine/Cargo.toml index 2c06df8cd..2fd7a6a82 100644 --- a/libs/machine/Cargo.toml +++ b/libs/machine/Cargo.toml @@ -38,7 +38,7 @@ libkernel-state = { workspace = true } liblocal-irq = { workspace = true } liblocking = { workspace = true } liblog = { workspace = true } -libmemory = { workspace = true } +# libmemory = { workspace = true } libplatform = { workspace = true } libprimitives = { workspace = true } libqemu = { workspace = true } diff --git a/libs/memory/Cargo.toml b/libs/memory/Cargo.toml index ffc752eed..bbf6aa969 100644 --- a/libs/memory/Cargo.toml +++ b/libs/memory/Cargo.toml @@ -20,20 +20,11 @@ maintenance = { status = "experimental" } [dependencies] aarch64-cpu = { workspace = true } -bit_field = { workspace = true } -bitflags = { workspace = true } -buddy-alloc = { workspace = true } -cfg-if = { workspace = true } libaddress = { workspace = true } -liblocking = { workspace = true } liblog = { workspace = true } libmapping = { workspace = true } -num = { workspace = true } -once_cell = { workspace = true } snafu = { workspace = true } tock-registers = { workspace = true } -usize_conversions = { workspace = true } -ux = { workspace = true } [lib] test = false diff --git a/libs/memory/src/arch/aarch64/mmu.rs b/libs/memory/src/arch/aarch64/mmu.rs index 95a9099d8..cbec7fa2a 100644 --- a/libs/memory/src/arch/aarch64/mmu.rs +++ b/libs/memory/src/arch/aarch64/mmu.rs @@ -1,12 +1,15 @@ +/* + * SPDX-License-Identifier: BlueOak-1.0.0 + * Copyright (c) Berkus Decker + */ + +//! AArch64 MMU hardware configuration and enablement. +//! +//! This module handles the actual hardware register setup (MAIR, TCR, SCTLR) +//! and MMU enable sequence. It does NOT manage translation table contents — +//! that is handled by the `Table` type and `Aarch64_4K` arch implementation. + use { - crate::{ - AddressSpace, MMUEnableError, TranslationGranule, - arch::translation_table::{ - PageFlags, PageSize, STAGE1_PAGE_DESCRIPTOR, STAGE1_TABLE_DESCRIPTOR, Size2MiB, - Size4KiB, TableFlags, - }, - interface, - }, aarch64_cpu::{ asm::{self, barrier}, registers::{ID_AA64MMFR0_EL1, SCTLR_EL1, TCR_EL1}, @@ -14,64 +17,48 @@ use { core::intrinsics::unlikely, libaddress::{Address, Physical}, liblog::println, - libmapping::AttributeFields, tock_registers::interfaces::{ReadWriteable, Readable, Writeable}, }; -//-------------------------------------------------------------------------------------------------- -// Private Definitions -//-------------------------------------------------------------------------------------------------- - -/// Memory Management Unit type. -struct MemoryManagementUnit; +/// MMU enable errors. +#[derive(Debug)] +pub enum MMUEnableError { + /// MMU is already enabled. + AlreadyEnabled, + /// Other error. + Other { err: &'static str }, +} -//-------------------------------------------------------------------------------------------------- -// Public Definitions -//-------------------------------------------------------------------------------------------------- +impl core::fmt::Display for MMUEnableError { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + match self { + Self::AlreadyEnabled => write!(f, "MMU is already enabled"), + Self::Other { err } => write!(f, "{err}"), + } + } +} -pub type Granule512MiB = TranslationGranule<{ 512 * 1024 * 1024 }>; -pub type Granule64KiB = TranslationGranule<{ 64 * 1024 }>; +/// Memory Management Unit type. +pub struct MemoryManagementUnit; -/// Constants for indexing the MAIR_EL1. -#[allow(dead_code)] +/// MAIR_EL1 attribute indices. +/// +/// These must match the MAIR setup in `set_up_mair()` and the indices +/// used by `arch::aarch64::translation::encode_attributes()`. pub mod mair { - // Three descriptive consts for indexing into the correct MAIR_EL1 attributes. pub mod attr { pub const NORMAL: u64 = 0; pub const NORMAL_NON_CACHEABLE: u64 = 1; pub const DEVICE_NGNRE: u64 = 2; - // DEVICE_GRE - // DEVICE_NGNRNE } } -//-------------------------------------------------------------------------------------------------- -// Global instances -//-------------------------------------------------------------------------------------------------- - static MMU: MemoryManagementUnit = MemoryManagementUnit; -//-------------------------------------------------------------------------------------------------- -// Private Implementations -//-------------------------------------------------------------------------------------------------- - -impl AddressSpace { - /// Checks for architectural restrictions. - pub const fn arch_address_space_size_sanity_checks() { - // Size must be at least one full 512 MiB table. - assert!(AS_SIZE.is_multiple_of(Granule512MiB::SIZE)); // assert!() is const-friendly - - // Check for 48 bit virtual address size as maximum, which is supported by any ARMv8 - // version. - assert!(AS_SIZE <= (1 << 48)); - } -} - impl MemoryManagementUnit { /// Setup function for the MAIR_EL1 register. fn set_up_mair(&self) { use aarch64_cpu::registers::MAIR_EL1; - // Define the three memory types that we will map: Normal DRAM, Uncached and device. MAIR_EL1.write( // Attribute 2 -- Device Memory MAIR_EL1::Attr2_Device::nonGathering_nonReordering_EarlyWriteAck @@ -86,61 +73,56 @@ impl MemoryManagementUnit { /// Configure various settings of stage 1 of the EL1 translation regime. fn configure_translation_control(&self) { - // TCR_EL1.{SH0, ORGN0, IRGN0, SH1, ORGN1, IRGN1} fields define memory region attributes for the - // translation table walk, for each of TTBR0_EL1 and TTBR1_EL1. - // For the Secure and Non-secure EL1&0 stage 1 translations, each of TTBR0_EL1 and TTBR1_EL1 - // contains an ASID field, and the TCR_EL1.A1 field selects which ASID to use. - - // Two-level tables with a 4Kb granule size may address ONLY 1Gb of virtual addresses. - // This seems to be not enough for RPi4? Try using tables from level 1 (TxSZ=below 34 bits), up to 512Gb - - // Configure various settings of stage 1 of the EL1 translation regime. - // PARange is 4 bits, ips is 3 bits @todo validate the range is acceptable. let ips = ID_AA64MMFR0_EL1.read(ID_AA64MMFR0_EL1::PARange); // Maximum 8Gb user VA let user_va_bits = 33; // ARMv8ARM Table D5-11 minimum TxSZ for starting table level 1 // Maximum 8Gb kernel VA - let kernel_va_bits = 33; // ARMv8ARM Table D5-11 minimum TxSZ for starting table level 1 + let kernel_va_bits = 33; TCR_EL1.write( - TCR_EL1::TBI0::Ignored // Top byte ignored, can be used for tagging. - + TCR_EL1::IPS.val(ips) // Intermediate Physical Address Size + TCR_EL1::TBI0::Ignored + + TCR_EL1::IPS.val(ips) // ttbr0 user memory addresses - + TCR_EL1::TG0::KiB_4 // 4 KiB granule + + TCR_EL1::TG0::KiB_4 + TCR_EL1::SH0::Inner + TCR_EL1::ORGN0::WriteBack_ReadAlloc_WriteAlloc_Cacheable + TCR_EL1::IRGN0::WriteBack_ReadAlloc_WriteAlloc_Cacheable + TCR_EL1::EPD0::EnableTTBR0Walks - + TCR_EL1::T0SZ.val(64 - user_va_bits) // ARMv8ARM Table D5-11 minimum TxSZ for starting table level 2 + + TCR_EL1::T0SZ.val(64 - user_va_bits) // ttbr1 kernel memory addresses - + TCR_EL1::TBI1::Ignored // Top byte ignored, can be used for tagging. @todo remove! - + TCR_EL1::TG1::KiB_4 // 4 KiB granule + + TCR_EL1::TBI1::Ignored + + TCR_EL1::TG1::KiB_4 + TCR_EL1::SH1::Inner + TCR_EL1::ORGN1::WriteBack_ReadAlloc_WriteAlloc_Cacheable + TCR_EL1::IRGN1::WriteBack_ReadAlloc_WriteAlloc_Cacheable - + TCR_EL1::EPD1::DisableTTBR1Walks // @fixme disabled for now - + TCR_EL1::T1SZ.val(64 - kernel_va_bits), // ARMv8ARM Table D5-11 minimum TxSZ for starting table level 2 + + TCR_EL1::EPD1::DisableTTBR1Walks // disabled for now + + TCR_EL1::T1SZ.val(64 - kernel_va_bits), ); } -} -//-------------------------------------------------------------------------------------------------- -// Public Implementations -//-------------------------------------------------------------------------------------------------- + fn is_enabled(&self) -> bool { + SCTLR_EL1.matches_all(SCTLR_EL1::M::Enable) + } +} /// Return a reference to the MMU instance. -pub fn mmu() -> &'static impl interface::MMU { +pub fn mmu() -> &'static MemoryManagementUnit { &MMU } -//------------------------------------------------------------------------------ -// OS Interface Code -//------------------------------------------------------------------------------ - -impl interface::MMU for MemoryManagementUnit { - unsafe fn enable_mmu_and_caching( +impl MemoryManagementUnit { + /// Turns on the MMU and enables data and instruction caching. + /// + /// `phys_tables_base_addr` is the physical address of the root translation + /// table (L0 or L1 depending on T0SZ configuration). + /// + /// # Safety + /// + /// - Changes the hardware's global state. + /// - The caller must ensure translation tables are properly populated. + pub unsafe fn enable_mmu_and_caching( &self, _phys_tables_base_addr: Address, ) -> Result<(), MMUEnableError> { @@ -149,397 +131,55 @@ impl interface::MMU for MemoryManagementUnit { } // Fail early if translation granule is not supported. - if unlikely(!ID_AA64MMFR0_EL1.matches_all(ID_AA64MMFR0_EL1::TGran64::Supported)) { + if unlikely(!ID_AA64MMFR0_EL1.matches_all(ID_AA64MMFR0_EL1::TGran4::Supported)) { return Err(MMUEnableError::Other { - err: "Translation granule not supported by hardware", + err: "4KiB translation granule not supported by hardware", }); } // Prepare the memory attribute indirection register. self.set_up_mair(); - // // Populate translation tables. - // KERNEL_TABLES - // .populate_translation_table_entries() - // .map_err(|err| MMUEnableError::Other { err })?; - - // from https://lore.kernel.org/all/db9612a7-9354-2357-9083-1d923b4d11e1@linaro.org/T/ - // The ARMv8.2-TTCNP extension allows an implementation to optimize by - // sharing TLB entries between multiple cores, provided that software - // declares that it's ready to deal with this by setting a CnP bit in - // the TTBRn_ELx. It is mandatory from ARMv8.2 onward. - - // support feature flag is in ID_AA64MMFR2 - // https://developer.arm.com/documentation/ddi0601/2022-03/AArch64-Registers/ID-AA64MMFR2-EL1--AArch64-Memory-Model-Feature-Register-2?lang=en - // CnP bits 3:0 - // From Armv8.2, the only permitted value is 0b0001. - // (this should be set to share the TLBs across cores.) - - // Point to the LVL2 table base address in TTBR0. - // TODO: USER_TABLES, not KERNEL_TABLES here? - // TTBR0_EL1.set_baddr(KERNEL_TABLES.entries.base_addr_u64()); // User (lo-)space addresses + // TODO: Set TTBR0_EL1 and TTBR1_EL1 to point to the root table. + // TTBR0_EL1.set_baddr(phys_tables_base_addr.as_u64()); // TTBR0_EL1.modify(TTBR0_EL1::CnP.val(1)); - // TTBR1_EL1.set_baddr(LVL1_TABLE.entries.base_addr_u64()); // Kernel (hi-)space addresses - // TTBR1_EL1.modify(TTBR1_EL1::CnP.val(1)); - - // upper half, kernel space - // asm volatile ("msr ttbr1_el1, %0" : : "r" ((unsigned long)&_end + TTBR_CNP + PAGESIZE)); - self.configure_translation_control(); // Switch the MMU on. - // // First, force all previous changes to be seen before the MMU is enabled. - // See [ARM ARM](https://developer.arm.com/documentation/den0024/a/The-Memory-Management-Unit/The-Translation-Lookaside-Buffer). - barrier::dsb(barrier::ISHST); // ensure write has completed - - // core::arch::asm!("tlbi alle1"); // invalidate all TLB entries -- must do it from EL2/EL3 - - barrier::dsb(barrier::ISH); // ensure completion of TLB invalidation - barrier::isb(barrier::SY); // synchronize context and ensure that no instructions are - // fetched using the old translation + barrier::dsb(barrier::ISHST); + barrier::dsb(barrier::ISH); + barrier::isb(barrier::SY); - // use cortex_a::regs::RegisterReadWrite; // Enable the MMU and turn on data and instruction caching. - SCTLR_EL1.modify( - SCTLR_EL1::EE::LittleEndian // Endianness select in EL1 - + SCTLR_EL1::E0E::LittleEndian // Endianness select in EL0 - + SCTLR_EL1::WXN::Disable // Writable means Execute Never - + SCTLR_EL1::SA::Disable // SP Alignment check in EL1, 16 byte align - + SCTLR_EL1::SA0::Disable // SP Alignment check in EL0, 16 byte align - + SCTLR_EL1::A::Disable // No alignment checks - + SCTLR_EL1::UCI::Trap // Unified Cache instructions trap - + SCTLR_EL1::UCT::Trap // CTR_EL0 instructions trap - + SCTLR_EL1::UMA::Trap // User Mask Access, trap on DAIF access - + SCTLR_EL1::NTWE::Trap // WFE/WFET instruction trap - + SCTLR_EL1::NTWI::Trap // WFI/WFIT instruction trap - + SCTLR_EL1::DZE::Trap // DC ZVA/GVA/GZVA instructions trap + SCTLR_EL1::EE::LittleEndian + + SCTLR_EL1::E0E::LittleEndian + + SCTLR_EL1::WXN::Disable + + SCTLR_EL1::SA::Disable + + SCTLR_EL1::SA0::Disable + + SCTLR_EL1::A::Disable + + SCTLR_EL1::UCI::Trap + + SCTLR_EL1::UCT::Trap + + SCTLR_EL1::UMA::Trap + + SCTLR_EL1::NTWE::Trap + + SCTLR_EL1::NTWI::Trap + + SCTLR_EL1::DZE::Trap + SCTLR_EL1::C::Cacheable + SCTLR_EL1::I::Cacheable + SCTLR_EL1::M::Enable, ); - // from https://forums.raspberrypi.com/viewtopic.php?t=320120#p1917769 - // Another hint: once the MMU has been activated you should let 2 CPU cycles pass and then call - // `tlbi alle2` to ensure the MMU related cache will be invalidated and the new settings are picked up. - + // Let 2 CPU cycles pass then invalidate TLB. asm::nop(); asm::nop(); - //TODO: tlbi - // Force MMU init to complete before next instruction - /* - * Invalidate the local I-cache so that any instructions fetched - * speculatively from the PoC are discarded, since they may have - * been dynamically patched at the PoU. - */ - // core::arch::asm!("tlbi alle1"); // invalidate all TLB entries -- must do it from EL2/EL3 - - // FIXME compiler happily inserts an instruction before this one... perhaps a compiler_fence()? - barrier::dsb(barrier::ISH); // ensure completion of TLB invalidation - barrier::isb(barrier::SY); // synchronize context and ensure that no instructions are fetched using the old translation + barrier::dsb(barrier::ISH); + barrier::isb(barrier::SY); println!("MMU activated"); Ok(()) } - - #[inline(always)] - fn is_enabled(&self) -> bool { - SCTLR_EL1.matches_all(SCTLR_EL1::M::Enable) - } - - fn print_features(&self) { - todo!() - } -} - -/// Type-safe enum wrapper covering Table's 64-bit entries. -#[derive(Clone)] -// #[repr(transparent)] -enum PageTableEntry { - /// Empty page table entry. - Invalid, - /// Table descriptor is a L0, L1 or L2 table pointing to another table. - /// L0 tables can only point to L1 tables. - /// A descriptor pointing to the next page table. - TableDescriptor(TableFlags), - /// A Level2 block descriptor with 2 MiB aperture. - /// - /// The output points to physical memory. - Lvl2BlockDescriptor(TableFlags), - /// A page PageTableEntry::descriptor with 4 KiB aperture. - /// - /// The output points to physical memory. - PageDescriptor(PageFlags), -} - -// A descriptor pointing to the next page table. (within PageTableEntry enum) -// struct TableDescriptor(register::FieldValue); - -impl PageTableEntry { - fn new_table_descriptor(next_lvl_table_addr: usize) -> Result { - if next_lvl_table_addr % Size4KiB::SIZE as usize != 0 { - // @todo SIZE must be usize - return Err("TableDescriptor: Address is not 4 KiB aligned."); - } - - let shifted = next_lvl_table_addr >> Size4KiB::SHIFT; - - Ok(PageTableEntry::TableDescriptor( - STAGE1_TABLE_DESCRIPTOR::VALID::True - + STAGE1_TABLE_DESCRIPTOR::TYPE::Table - + STAGE1_TABLE_DESCRIPTOR::NEXT_LEVEL_TABLE_ADDR_4KiB.val(shifted as u64), - )) - } -} - -#[derive(Debug)] //Snafu, -enum PageTableError { - // #[snafu(display("BlockDescriptor: Address is not 2 MiB aligned."))] - //"PageDescriptor: Address is not 4 KiB aligned." - NotAligned(&'static str), -} - -// A Level2 block descriptor with 2 MiB aperture. -// -// The output points to physical memory. -// struct Lvl2BlockDescriptor(register::FieldValue); - -impl PageTableEntry { - fn new_lvl2_block_descriptor( - output_addr: usize, - _attribute_fields: AttributeFields, - ) -> Result { - if output_addr % Size2MiB::SIZE as usize != 0 { - return Err(PageTableError::NotAligned(Size2MiB::SIZE_AS_DEBUG_STR)); - } - - let shifted = output_addr >> Size2MiB::SHIFT; - - Ok(PageTableEntry::Lvl2BlockDescriptor( - STAGE1_TABLE_DESCRIPTOR::VALID::True - // + STAGE1_TABLE_DESCRIPTOR::AF::Accessed - // + into_mmu_attributes(attribute_fields) - + STAGE1_TABLE_DESCRIPTOR::TYPE::Block - + STAGE1_TABLE_DESCRIPTOR::NEXT_LEVEL_TABLE_ADDR_4KiB.val(shifted as u64), - )) - } -} - -// A page descriptor with 4 KiB aperture. -// -// The output points to physical memory. - -impl PageTableEntry { - fn new_page_descriptor( - output_addr: usize, - _attribute_fields: AttributeFields, - ) -> Result { - if output_addr % Size4KiB::SIZE as usize != 0 { - return Err(PageTableError::NotAligned(Size4KiB::SIZE_AS_DEBUG_STR)); - } - - let shifted = output_addr >> Size4KiB::SHIFT; - - Ok(PageTableEntry::PageDescriptor( - STAGE1_PAGE_DESCRIPTOR::VALID::True - // + STAGE1_TABLE_DESCRIPTOR::AF::Accessed - // + into_mmu_attributes(attribute_fields) - + STAGE1_PAGE_DESCRIPTOR::TYPE::Page - + STAGE1_PAGE_DESCRIPTOR::OUTPUT_ADDR_4KiB.val(shifted as u64), - )) - } } - -impl From for PageTableEntry { - fn from(_val: u64) -> PageTableEntry { - // xxx0 -> Invalid - // xx11 -> TableDescriptor on L0, L1 and L2 - // xx10 -> Block Entry L1 and L2 - // xx11 -> PageDescriptor L3 - PageTableEntry::Invalid - } -} - -impl From for u64 { - fn from(val: PageTableEntry) -> u64 { - match val { - PageTableEntry::Invalid => 0, - PageTableEntry::TableDescriptor(x) | PageTableEntry::Lvl2BlockDescriptor(x) => x.value, - PageTableEntry::PageDescriptor(x) => x.value, - } - } -} - -// to get L0 we must allocate a few frames from boot region allocator. -// So, first we init the dtb, parse mem-regions from there, then init boot_info page and start mmu, -// this part will be inited in mmu::init(): - -// @todo do NOT keep these statically, always allocate from available bump memory -// static mut LVL2_TABLE: Table = Table:: { -// entries: [0; NUM_ENTRIES_4KIB as usize], -// level: PhantomData, -// }; - -// @todo do NOT keep these statically, always allocate from available bump memory -// static mut LVL3_TABLE: Table = Table:: { -// entries: [0; NUM_ENTRIES_4KIB as usize], -// level: PhantomData, -// }; - -trait BaseAddr { - fn base_addr_u64(&self) -> u64; - fn base_addr_usize(&self) -> usize; -} - -impl BaseAddr for [u64; 512] { - fn base_addr_u64(&self) -> u64 { - self as *const u64 as u64 - } - - fn base_addr_usize(&self) -> usize { - self as *const u64 as usize - } -} - -/// Set up identity mapped page tables for the first 1 gigabyte of address space. -/// default: 880 MB ARM ram, 128MB VC -/// -/// # Safety -/// -/// Completely unsafe, we're in the hardware land! Incorrectly initialised tables will just -/// restart the CPU. -pub unsafe fn init() -> Result<(), &'static str> { - // Prepare the memory attribute indirection register. - // mair::set_up(); - - // Point the first 2 MiB of virtual addresses to the follow-up LVL3 - // page-table. - // LVL2_TABLE.entries[0] = - // PageTableEntry::new_table_descriptor(LVL3_TABLE.entries.base_addr_usize())?.into(); - - // Fill the rest of the LVL2 (2 MiB) entries as block descriptors. - // - // Notice the skip(1) which makes the iteration start at the second 2 MiB - // block (0x20_0000). - // for (block_descriptor_nr, entry) in LVL2_TABLE.entries.iter_mut().enumerate().skip(1) { - // let virt_addr = block_descriptor_nr << Size2MiB::SHIFT; - - // let (output_addr, attribute_fields) = match get_virt_addr_properties(virt_addr) { - // Err(s) => return Err(s), - // Ok((a, b)) => (a, b), - // }; - - // let block_desc = - // match PageTableEntry::new_lvl2_block_descriptor(output_addr, attribute_fields) { - // Err(s) => return Err(s), - // Ok(desc) => desc, - // }; - - // *entry = block_desc.into(); - // } - - // Finally, fill the single LVL3 table (4 KiB granule). - // for (page_descriptor_nr, entry) in LVL3_TABLE.entries.iter_mut().enumerate() { - // let virt_addr = page_descriptor_nr << Size4KiB::SHIFT; - - // let (output_addr, attribute_fields) = match get_virt_addr_properties(virt_addr) { - // Err(s) => return Err(s), - // Ok((a, b)) => (a, b), - // }; - - // let page_desc = match PageTableEntry::new_page_descriptor(output_addr, attribute_fields) { - // Err(s) => return Err(s), - // Ok(desc) => desc, - // }; - - // *entry = page_desc.into(); - // } - - // Point to the LVL2 table base address in TTBR0. - // TTBR0_EL1.set_baddr(LVL2_TABLE.entries.base_addr_u64()); // User (lo-)space addresses - - // TTBR1_EL1.set_baddr(LVL2_TABLE.entries.base_addr_u64()); // Kernel (hi-)space addresses - - // Configure various settings of stage 1 of the EL1 translation regime. - let ips = ID_AA64MMFR0_EL1.read(ID_AA64MMFR0_EL1::PARange); - TCR_EL1.write( - TCR_EL1::TBI0::Ignored // @todo TBI1 also set to Ignored?? - + TCR_EL1::IPS.val(ips) // Intermediate Physical Address Size - // ttbr0 user memory addresses - + TCR_EL1::TG0::KiB_4 // 4 KiB granule - + TCR_EL1::SH0::Inner - + TCR_EL1::ORGN0::WriteBack_ReadAlloc_WriteAlloc_Cacheable - + TCR_EL1::IRGN0::WriteBack_ReadAlloc_WriteAlloc_Cacheable - + TCR_EL1::EPD0::EnableTTBR0Walks - + TCR_EL1::T0SZ.val(34) // ARMv8ARM Table D5-11 minimum TxSZ for starting table level 2 - // ttbr1 kernel memory addresses - + TCR_EL1::TG1::KiB_4 // 4 KiB granule - + TCR_EL1::SH1::Inner - + TCR_EL1::ORGN1::WriteBack_ReadAlloc_WriteAlloc_Cacheable - + TCR_EL1::IRGN1::WriteBack_ReadAlloc_WriteAlloc_Cacheable - + TCR_EL1::EPD1::EnableTTBR1Walks - + TCR_EL1::T1SZ.val(34), // ARMv8ARM Table D5-11 minimum TxSZ for starting table level 2 - ); - - // Switch the MMU on. - // - // First, force all previous changes to be seen before the MMU is enabled. - barrier::isb(barrier::SY); - - // use cortex_a::regs::RegisterReadWrite; - // Enable the MMU and turn on data and instruction caching. - SCTLR_EL1.modify(SCTLR_EL1::M::Enable + SCTLR_EL1::C::Cacheable + SCTLR_EL1::I::Cacheable); - - // Force MMU init to complete before next instruction - /* - * Invalidate the local I-cache so that any instructions fetched - * speculatively from the PoC are discarded, since they may have - * been dynamically patched at the PoU. - */ - barrier::isb(barrier::SY); - - Ok(()) -} - -// A function that maps the generic memory range attributes to HW-specific -// attributes of the MMU. -// fn into_mmu_attributes( -// attribute_fields: AttributeFields, -// ) -> FieldValue { -// use super::{AccessPermissions, MemAttributes}; - -// // Memory attributes -// let mut desc = match attribute_fields.mem_attributes { -// MemAttributes::CacheableDRAM => { -// STAGE1_DESCRIPTOR::SH::InnerShareable -// + STAGE1_DESCRIPTOR::AttrIndx.val(mair::attr::NORMAL) -// } -// MemAttributes::NonCacheableDRAM => { -// STAGE1_DESCRIPTOR::SH::InnerShareable -// + STAGE1_DESCRIPTOR::AttrIndx.val(mair::attr::NORMAL_NON_CACHEABLE) -// } -// MemAttributes::Device => { -// STAGE1_DESCRIPTOR::SH::OuterShareable -// + STAGE1_DESCRIPTOR::AttrIndx.val(mair::attr::DEVICE_NGNRE) -// } -// }; - -// // Access Permissions -// desc += match attribute_fields.acc_perms { -// AccessPermissions::ReadOnly => STAGE1_DESCRIPTOR::AP::RO_EL1, -// AccessPermissions::ReadWrite => STAGE1_DESCRIPTOR::AP::RW_EL1, -// }; - -// // Execute Never -// desc += if attribute_fields.execute_never { -// STAGE1_DESCRIPTOR::PXN::NeverExecute -// } else { -// STAGE1_DESCRIPTOR::PXN::Execute -// }; - -// desc -// } diff --git a/libs/memory/src/arch/aarch64/mmu2.rs b/libs/memory/src/arch/aarch64/mmu2.rs deleted file mode 100644 index 7548cbf7d..000000000 --- a/libs/memory/src/arch/aarch64/mmu2.rs +++ /dev/null @@ -1,631 +0,0 @@ -/* - * SPDX-License-Identifier: BlueOak-1.0.0 - * Copyright (c) Berkus Decker - */ - -//! MMU initialisation. -//! -//! Paging is mostly based on [previous version](https://os.phil-opp.com/page-tables/) of -//! Phil Opp's [paging guide](https://os.phil-opp.com/paging-implementation/) and -//! [ARMv8 ARM memory addressing](https://static.docs.arm.com/100940/0100/armv8_a_address%20translation_100940_0100_en.pdf). -//! It includes ideas from Sergio Benitez' cs140e OSdev course material on type-safe access. - -#![allow(dead_code)] - -use { - crate::memory::{PhysAddr, VirtAddr, PhysFrame, PageSize}, - core::{ - marker::PhantomData, - ops::{Index, IndexMut}, - ptr::Unique, - }, - snafu::Snafu, - tock_registers::{fields, register_bitfields, LocalRegisterCopy}, -}; - -#[derive(Debug, Snafu)] -enum MmuError {} - -/* -/// Set up identity mapped page tables for the first 1 gigabyte of address space. -/// default: 880 MB ARM ram, 128MB VC -/// -/// # Safety -/// -/// Completely unsafe, we're in the hardware land! Incorrectly initialised tables will just -/// restart the CPU. -pub fn init() -> Result<(), MmuError> { - // Prepare the "memory attribute indirection register". - mair::set_up(); - - // @todo Do not map entire RAM, map only loaded and allocated space. - // Also find and map framebuffer -- the user-mode framebuffer driver should be able to do that. - - // should receive in args an obtained memory map from DT - let memory_map = Regions { - start: 0x1000, - size: 0x10000, - }; - - // bump-allocate page tables for entire memory - // also allocate phys memory to kernel space! - // - // separate regions - regular memory, device mmaps, - // initial thread maps ALL the memory?? - // instead - // init thread may map only necessary mem - // boot time only map kernel physmem space, and currently loaded kernel data - // PROBABLY only kernel mapping TTBR1 is needed, the rest is not very useful? - // take over protected memory space though anyway. - - // Point the first 2 MiB of virtual addresses to the follow-up LVL3 - // page-table. - // LVL2_TABLE.entries[0] = - // PageTableEntry::new_table_descriptor(LVL3_TABLE.entries.base_addr_usize())?.into(); - - // Fill the rest of the LVL2 (2 MiB) entries as block descriptors. - // - // Notice the skip(1) which makes the iteration start at the second 2 MiB - // block (0x20_0000). - for (block_descriptor_nr, entry) in LVL2_TABLE.entries.iter_mut().enumerate().skip(1) { - let virt_addr = block_descriptor_nr << Size2MiB::SHIFT; - - let (output_addr, attribute_fields) = match get_virt_addr_properties(virt_addr) { - Err(s) => return Err(s), - Ok((a, b)) => (a, b), - }; - - let block_desc = - match PageTableEntry::new_lvl2_block_descriptor(output_addr, attribute_fields) { - Err(s) => return Err(s), - Ok(desc) => desc, - }; - - *entry = block_desc.into(); - } - - // Finally, fill the single LVL3 table (4 KiB granule). - for (page_descriptor_nr, entry) in LVL3_TABLE.entries.iter_mut().enumerate() { - let virt_addr = page_descriptor_nr << Size4KiB::SHIFT; - - let (output_addr, attribute_fields) = match get_virt_addr_properties(virt_addr) { - Err(s) => return Err(s), - Ok((a, b)) => (a, b), - }; - - let page_desc = match PageTableEntry::new_page_descriptor(output_addr, attribute_fields) { - Err(s) => return Err(s), - Ok(desc) => desc, - }; - - *entry = page_desc.into(); - } -}*/ - -/* - * With 4k page granule, a virtual address is split into 4 lookup parts - * spanning 9 bits each: - * - * _______________________________________________ - * | | | | | | | - * | signx | Lv0 | Lv1 | Lv2 | Lv3 | off | - * |_______|_______|_______|_______|_______|_______| - * 63-48 47-39 38-30 29-21 20-12 11-00 - * - * mask page size - * - * Lv0: FF8000000000 -- - * Lv1: 7FC0000000 - * off: 3FFFFFFF 1G - * Lv2: 3FE00000 - * off: 1FFFFF 2M - * Lv3: 1FF000 - * off: FFF 4K - * - * RPi3 supports 64K and 4K granules, also 40-bit physical addresses. - * It also can address only 1G physical memory, so these 40-bit phys addresses are a fake. - * RPi4 can address more (up to 8Gb physical memory, 33 bits phys address, SoC has a so-called - * Full 35-bit address mode). - * - * 48-bit virtual address space; different mappings in VBAR0 (EL0) and VBAR1 (EL1+). - */ - -register_bitfields! { - u64, - VA_INDEX [ - LEVEL0 OFFSET(39) NUMBITS(9) [], - LEVEL1 OFFSET(30) NUMBITS(9) [], - LEVEL2 OFFSET(21) NUMBITS(9) [], - LEVEL3 OFFSET(12) NUMBITS(9) [], - OFFSET OFFSET(0) NUMBITS(12) [], - ] -} - -register_bitfields! { - u64, - // AArch64 Reference Manual page 2150, D5-2445 - TABLE_DESCRIPTOR [ - // In table descriptors - - NSTable_EL3 OFFSET(63) NUMBITS(1) [], - - /// Access Permissions for subsequent tables - APTable OFFSET(61) NUMBITS(2) [ - RW_EL1 = 0b00, - RW_EL1_EL0 = 0b01, - RO_EL1 = 0b10, - RO_EL1_EL0 = 0b11 - ], - - // User execute-never for subsequent tables - UXNTable OFFSET(60) NUMBITS(1) [ - Execute = 0, - NeverExecute = 1 - ], - - /// Privileged execute-never for subsequent tables - PXNTable OFFSET(59) NUMBITS(1) [ - Execute = 0, - NeverExecute = 1 - ], - - // In block descriptors - - // OS-specific data - OSData OFFSET(55) NUMBITS(4) [], - - // User execute-never - UXN OFFSET(54) NUMBITS(1) [ - Execute = 0, - NeverExecute = 1 - ], - - /// Privileged execute-never - PXN OFFSET(53) NUMBITS(1) [ - Execute = 0, - NeverExecute = 1 - ], - - // @fixme ?? where is this described - CONTIGUOUS OFFSET(52) NUMBITS(1) [ - False = 0, - True = 1 - ], - - // @fixme ?? where is this described - DIRTY OFFSET(51) NUMBITS(1) [ - False = 0, - True = 1 - ], - - /// Various address fields, depending on use case - LVL2_OUTPUT_ADDR_4KiB OFFSET(21) NUMBITS(27) [], // [47:21] - NEXT_LVL_TABLE_ADDR_4KiB OFFSET(12) NUMBITS(36) [], // [47:12] - - // @fixme ?? where is this described - NON_GLOBAL OFFSET(11) NUMBITS(1) [ - False = 0, - True = 1 - ], - - /// Access flag - AF OFFSET(10) NUMBITS(1) [ - False = 0, - True = 1 - ], - - /// Share-ability field - SH OFFSET(8) NUMBITS(2) [ - OuterShareable = 0b10, - InnerShareable = 0b11 - ], - - /// Access Permissions - AP OFFSET(6) NUMBITS(2) [ - RW_EL1 = 0b00, - RW_EL1_EL0 = 0b01, - RO_EL1 = 0b10, - RO_EL1_EL0 = 0b11 - ], - - NS_EL3 OFFSET(5) NUMBITS(1) [], - - /// Memory attributes index into the MAIR_EL1 register - AttrIndx OFFSET(2) NUMBITS(3) [], - - TYPE OFFSET(1) NUMBITS(1) [ - Block = 0, - Table = 1 - ], - - VALID OFFSET(0) NUMBITS(1) [ - False = 0, - True = 1 - ] - ] -} - -// type VaIndex = tock_registers::fields::FieldValue; -type VaType = LocalRegisterCopy; -type EntryFlags = fields::FieldValue; -type EntryRegister = LocalRegisterCopy; - -// Possible mappings: -// * TTBR0 pointing to user page global directory -// * TTBR0 pointing to user page upper directory (only if mmu is set up differently) -// * TTBR1 pointing to kernel page global directory with full physmem access - -/*! - * Paging system uses a separate address space in top kernel region (TTBR1) to access - * entire physical memory contents. - * This mapping is not available to user space (user space uses TTBR0). - * - * Use the largest possible granule size to map physical memory since we want to use - * the least amount of memory for these mappings. - */ - -// TTBR0 Page Global Directory - -// Level 0 descriptors can only output the address of a Level 1 table. -// Level 3 descriptors cannot point to another table and can only output block addresses. -// The format of the table is therefore slightly different for Level 3. -// -// this means: -// - in level 0 page table can be only TableDescriptors -// - in level 1,2 page table can be TableDescriptors, Lvl2BlockDescriptors (PageDescriptors) -// - in level 3 page table can be only PageDescriptors - -// Level / Types | Table Descriptor | Lvl2BlockDescriptor (PageDescriptor) -// --------------+------------------+-------------------------------------- -// 0 | X | (with 4KiB granule) -// 1 | X | X (1GiB range) -// 2 | X | X (2MiB range) -// 3 | | X (4KiB range) -- called PageDescriptor -// encoding actually the same as in Table Descriptor - -// Translation granule affects the size of the block addressed. -// Lets use 4KiB granule on RPi3 for simplicity. - -// 1, set 4KiB granule size to use the PGD - we could use 16KiB granule instead? -// - need to measure waste level -// - but lets stick with 4KiB for now -// - -// If I have, for example, Table I can get from it N `Table` (via impl HierarchicalTable) -// From Table I can get either `Table` (via impl HierarchicalTable) or `BlockDescriptor` -// From Table I can get either `Table` (via impl HierarchicalTable) or `BlockDescriptor` -// From Table I can only get `PageDescriptor` (because no impl HierarchicalTable exists) - -/// GlobalDirectory [ UpperDirectory entries ] -/// UpperDirectory [ PageDirectory | GiantPage ] -/// PageDirectory [ PageTable | LargePage ] -/// PageTable [ PageFrames ] - -// do those as separate types, then in accessors allow only certain combinations -// e.g. -// struct UpperDirectoryEntry; // DirectoryEntry -// struct PageDirectoryEntry; // DirectoryEntry -// struct GiantPageFrame; // PageFrame -// struct PageTableEntry; // DirectoryEntry -// struct LargePageFrame; // PageFrame -// struct PageFrame; // PageFrame - -// enum PageTableEntry { Page(&mut PageDescriptor), Block(&mut BlockDescriptor), Etc(&mut u64), Invalid(&mut u64) } -// impl PageTabelEntry { fn new_from_entry_addr(&u64) } -// return enum PageTableEntry constructed from table bits in u64 - -enum L0Entries { - UpperDirectoryEntry(VirtAddr), -} -enum L1Entries { - PageDirectoryEntry(VirtAddr), - GiantPageFrame(PhysFrame), -} -enum L2Entries { - PageTableEntry(VirtAddr), - LargePageFrame(PhysFrame), -} -enum L3Entries { - PageFrame(PhysFrame), -} - -enum Frames { - GiantPageFrame, - LargePageFrame, - PageFrame, -} - -// ---- -// ---- -// ---- Table levels -// ---- -// ---- - -/// L0 table -- only pointers to L1 tables -pub enum L0PageGlobalDirectory {} -/// L1 tables -- pointers to L2 tables or giant 1GiB pages -pub enum L1PageUpperDirectory {} -/// L2 tables -- pointers to L3 tables or huge 2MiB pages -pub enum L2PageDirectory {} -/// L3 tables -- only pointers to 4/16KiB pages -pub enum L3PageTable {} - -/// Shared trait for specific table levels. -pub trait TableLevel {} - -/// Shared trait for hierarchical table levels. -/// -/// Specifies what is the next level of page table hierarchy. -pub trait HierarchicalLevel: TableLevel { - /// Level of the next translation table below this one. - type NextLevel: TableLevel; - - // fn translate() -> Directory; -} - -/// Specify allowed page size for each level. -pub trait HierarchicalPageLevel: TableLevel { - /// Size of the page that can be contained in this table level. - type PageLevel: PageSize; -} - -impl TableLevel for L0PageGlobalDirectory {} -impl TableLevel for L1PageUpperDirectory {} -impl TableLevel for L2PageDirectory {} -impl TableLevel for L3PageTable {} - -impl HierarchicalLevel for L0PageGlobalDirectory { - type NextLevel = L1PageUpperDirectory; -} -impl HierarchicalLevel for L1PageUpperDirectory { - type NextLevel = L2PageDirectory; -} -impl HierarchicalLevel for L2PageDirectory { - type NextLevel = L3PageTable; -} -// L3PageTables do not have next level, therefore they are not HierarchicalLevel - -// L0PageGlobalDirectory does not contain pages, so they are not HierarchicalPageLevel -impl HierarchicalPageLevel for L1PageUpperDirectory { - type PageLevel = Page; -} -impl HierarchicalPageLevel for L2PageDirectory { - type PageLevel = Page; -} -impl HierarchicalPageLevel for L3PageTable { - type PageLevel = Page; -} - -// ---- -// ---- -// ---- Directory -// ---- -// ---- - -// Maximum OA is 48 bits. -// -// Level 0 table descriptor has Output Address in [47:12] --> level 1 table -// Level 0 descriptor cannot be block descriptor. -// -// Level 1 table descriptor has Output Address in [47:12] --> level 2 table -// Level 1 block descriptor has Output Address in [47:30] -// -// Level 2 table descriptor has Output Address in [47:12] --> level 3 table -// Level 2 block descriptor has Output Address in [47:21] -// -// Level 3 block descriptor has Output Address in [47:12] -// Upper Attributes [63:51] -// Res0 [50:48] -// Lower Attributes [11:2] -// 11b [1:0] - -// Each table consists of 2**9 entries -const TABLE_BITS: usize = 9; -const INDEX_MASK: usize = (1 << TABLE_BITS) - 1; - -static_assertions::const_assert!(INDEX_MASK == 0x1ff); - -// @todo Table in mmu.rs -/// MMU address translation table. -/// Contains just u64 internally, provides enum interface on top -#[repr(C)] -#[repr(align(4096))] -struct Directory { - entries: [u64; 1 << TABLE_BITS], - level: PhantomData, -} - -impl Directory { - fn next(&self, address: VirtAddr) -> Option { - let va = VaType::new(address.as_u64()); - let index = va.read(VA_INDEX::LEVEL0); - match self.next_table_address(index as usize) { - Some(phys_addr) => { - Some(L0Entries::UpperDirectoryEntry(phys_addr.user_to_kernel())) - } - None => None, - } - } -} - -impl Directory { - fn next(&self, address: VirtAddr) -> Option { - let va = VaType::new(address.as_u64()); - let index = va.read(VA_INDEX::LEVEL1); - match self.next_table_address(index as usize) { - Some(phys_addr) => { - Some(L1Entries::PageDirectoryEntry(phys_addr.user_to_kernel())) - } - None => None, // @todo could be 1GiB frame - } - } -} - -impl Directory { - fn next(&self, address: VirtAddr) -> Option { - let va = VaType::new(address.as_u64()); - let index = va.read(VA_INDEX::LEVEL2); - match self.next_table_address(index as usize) { - Some(phys_addr) => { - Some(L2Entries::PageTableEntry(phys_addr.user_to_kernel())) - } - None => None, // @todo could be 2MiB frame - } - } -} - -impl Directory { - fn next(&self, address: VirtAddr) -> Option { - let va = VaType::new(address.as_u64()); - let _index = va.read(VA_INDEX::LEVEL3); - // @fixme wrong function - // match self.next_table_address(index as usize) { - // Some(phys_addr) => Some(L3Entries::PageFrame(phys_addr.user_to_kernel())), - // None => None, // Nothing there - // } - None - } -} - -// Implementation code shared for all levels of page tables -impl Directory -where - Level: TableLevel, -{ - /// Construct a zeroed table at given physical location. - // unsafe fn at(location: PhysAddr) -> &Self {} - - /// Construct and return zeroed table. - fn zeroed() -> Self { - Self { - entries: [0; 1 << TABLE_BITS], - level: PhantomData, - } - } - - /// Zero out entire table. - pub fn zero(&mut self) { - for entry in self.entries.iter_mut() { - *entry = 0; - } - } -} - -impl Index for Directory -where - Level: TableLevel, -{ - type Output = u64; - - fn index(&self, index: usize) -> &Self::Output { - &self.entries[index] - } -} - -impl IndexMut for Directory -where - Level: TableLevel, -{ - fn index_mut(&mut self, index: usize) -> &mut Self::Output { - &mut self.entries[index] - } -} - -impl Directory -where - Level: HierarchicalLevel, -{ - fn next_table_address(&self, index: usize) -> Option { - let entry_flags = EntryRegister::new(self[index]); - // If table entry has 0b11 mask set, it is a valid table entry. - // Address of the following table may be extracted from bits 47:12 - if entry_flags.matches_all(TABLE_DESCRIPTOR::VALID::True + TABLE_DESCRIPTOR::TYPE::Table) { - Some(PhysAddr::new( - entry_flags.read(TABLE_DESCRIPTOR::NEXT_LVL_TABLE_ADDR_4KiB) << Size4KiB::SHIFT, - )) - } else { - None - } - } - - pub fn next_table(&self, index: usize) -> Option<&Table> { - self.next_table_address(index) - .map(|address| unsafe { &*(address.user_to_kernel().as_ptr()) }) - } - - pub fn next_table_mut(&mut self, index: usize) -> Option<&mut Table> { - self.next_table_address(index) - .map(|address| unsafe { &mut *(address.user_to_kernel().as_mut_ptr()) }) - } - - pub fn translate_levels(&self, _address: VirtAddr) -> Option { - None - } -} - -// ---- -// ---- -// ---- VirtSpace -// ---- -// ---- - -/// Errors from mapping layer -#[derive(Debug, Snafu)] -pub enum TranslationError { - /// No page found. @todo - NoPage, -} - -/// Virtual address space. @todo -pub struct VirtSpace { - l0: Unique>, -} - -// translation steps: -// l0: upper page directory or Err() -// l1: lower page directory or 1Gb aperture or Err() -// l2: page table or 2MiB aperture or Err() -// l3: 4KiB aperture or Err() - -impl VirtSpace { - // Translate translates address all the way down to physical address or error. - // On each level there's next_table() fn that resolves to the next level table if possible. - pub fn translate(&self, virtual_address: VirtAddr) -> Result { - // let offset = virtual_address % Self::PageLevel::SIZE as usize; // use the size of the last page? - self.translate_page(Self::PageLevel::containing_address(virtual_address))? - .map(|frame, offset| frame.start_address() + offset) - } -} - -// pageglobaldirectory.translate() { -// get page index <- generic over page level (xx << (10 + (3 - level) * 9)) -// return page[index]?.translate(rest); -// } - -#[cfg(test)] -mod tests { - use super::*; - - #[test_case] - fn table_construction() { - let mut level0_table = Directory::::zeroed(); - let level1_table = Directory::::zeroed(); - let level2_table = Directory::::zeroed(); - let level3_table = Directory::::zeroed(); - - assert!(level0_table.next_table_address(0).is_none()); - - // Make entry map to a level1 table - level0_table[0] = EntryFlags::from( - TABLE_DESCRIPTOR::VALID::True - + TABLE_DESCRIPTOR::TYPE::Table - + TABLE_DESCRIPTOR::NEXT_LVL_TABLE_ADDR_4KiB.val(0x424242), - ) - .into(); - - assert!(level0_table.next_table_address(0).is_some()); - - let addr = level0_table.next_table_address(0).unwrap(); - assert_eq!(addr.as_u64(), (0x424242u64 << 12)); - } -} diff --git a/libs/memory/src/arch/aarch64/mmu_experimental.rs b/libs/memory/src/arch/aarch64/mmu_experimental.rs deleted file mode 100644 index ba56414d1..000000000 --- a/libs/memory/src/arch/aarch64/mmu_experimental.rs +++ /dev/null @@ -1,279 +0,0 @@ -// Check largest VA supported, calculate physical_memory_offset -// -const PHYSICAL_MEMORY_OFFSET: u64 = 0xffff_8000_0000_0000; // Last 1GiB of VA space - -// AArch64: -// Table D4-8-2021: check supported granule sizes, select alloc policy based on results. -// TTBR_ELx is the pdbr for specific page tables - -// Page 2068 actual page descriptor formats - -/// Errors from mapping layer -#[derive(Debug, Snafu)] -pub enum TranslationError { - NoPage, -} - -// Pointer to currently active page table -// Could be either user space (TTBR0) or kernel space (TTBR1) -- ?? -pub struct ActivePageTable { - l0: Unique>, -} - -impl ActivePageTable { - pub unsafe fn new() -> ActivePageTable { - ActivePageTable { - l0: Unique::new_unchecked(0 as *mut _), - } - } - - fn l0(&self) -> &Table { - unsafe { self.l0.as_ref() } - } - - fn l0_mut(&mut self) -> &mut Table { - unsafe { self.l0.as_mut() } - } - - // pub fn translate(&self, virtual_address: VirtAddr) -> Result { - // let offset = virtual_address % Size4KiB::SIZE as usize; // @todo use the size of the last page of course - // self.translate_page(Page::containing_address(virtual_address)) - // .map(|frame| frame.start_address() + offset) - // } - - fn translate_page(&self, page: Page) -> Result { - // @todo translate only one level of hierarchy per impl function... - let l1 = self.l0().next_table(u64::from(page.l0_index()) as usize); - /* - let huge_page = || { - l1.and_then(|l1| { - let l1_entry = &l1[page.l1_index() as usize]; - // 1GiB page? - if let Some(start_frame) = l1_entry.pointed_frame() { - if l1_entry.flags().read(STAGE1_DESCRIPTOR::TYPE) - != STAGE1_DESCRIPTOR::TYPE::Table.value - { - // address must be 1GiB aligned - //start_frame.is_aligned() - assert!(start_frame.number % (NUM_ENTRIES_4KIB * NUM_ENTRIES_4KIB) == 0); - return Ok(PhysFrame::from_start_address( - start_frame.number - + page.l2_index() * NUM_ENTRIES_4KIB - + page.l3_index(), - )); - } - } - if let Some(l2) = l1.next_table(page.l1_index()) { - let l2_entry = &l2[page.l2_index()]; - // 2MiB page? - if let Some(start_frame) = l2_entry.pointed_frame() { - if l2_entry.flags().read(STAGE1_DESCRIPTOR::TYPE) - != STAGE1_DESCRIPTOR::TYPE::Table - { - // address must be 2MiB aligned - assert!(start_frame.number % NUM_ENTRIES_4KIB == 0); - return Ok(PhysFrame::from_start_address( - start_frame.number + page.l3_index(), - )); - } - } - } - Err(TranslationError::NoPage) - }) - }; - */ - let v = l1 - .and_then(|l1| l1.next_table(u64::from(page.l1_index()) as usize)) - .and_then(|l2| l2.next_table(u64::from(page.l2_index()) as usize)) - .and_then(|l3| Some(l3[u64::from(page.l3_index()) as usize])); //.pointed_frame()) - // .ok_or(TranslationError::NoPage) - // .or_else(huge_page) - Ok(v.unwrap().into()) - } - - pub fn map_to(&mut self, page: Page, frame: PhysFrame, flags: EntryFlags, allocator: &mut A) - where - A: FrameAllocator, - { - let l0 = self.l0_mut(); - let l1 = l0.next_table_create(u64::from(page.l0_index()) as usize, allocator); - let l2 = l1.next_table_create(u64::from(page.l1_index()) as usize, allocator); - let l3 = l2.next_table_create(u64::from(page.l2_index()) as usize, allocator); - - assert_eq!( - l3[u64::from(page.l3_index()) as usize], - 0 /*.is_unused()*/ - ); - l3[u64::from(page.l3_index()) as usize] = PageTableEntry::PageDescriptor( - STAGE1_DESCRIPTOR::NEXT_LVL_TABLE_ADDR_4KiB.val(u64::from(frame)) - + flags // @todo properly extract flags - + STAGE1_DESCRIPTOR::VALID::True, - ) - .into(); - } - - pub fn map(&mut self, page: Page, flags: EntryFlags, allocator: &mut A) - where - A: FrameAllocator, - { - // @todo fail mapping if table is not allocated, causing client to allocate and restart - // @todo problems described in preso - chicken&egg problem of allocating first allocations - let frame = allocator.allocate_frame().expect("out of memory"); - self.map_to(page, frame, flags, allocator) - } - - pub fn identity_map(&mut self, frame: PhysFrame, flags: EntryFlags, allocator: &mut A) - where - A: FrameAllocator, - { - let page = Page::containing_address(VirtAddr::new(frame.start_address().as_u64())); - self.map_to(page, frame, flags, allocator) - } - - fn unmap(&mut self, page: Page, _allocator: &mut A) - where - A: FrameAllocator, - { - // use aarch64::instructions::tlb; - // use x86_64::VirtAddr; - - assert!(self.translate(page.start_address()).is_ok()); - - let l3 = self - .l0_mut() - .next_table_mut(u64::from(page.l0_index()) as usize) - .and_then(|l1| l1.next_table_mut(u64::from(page.l1_index()) as usize)) - .and_then(|l2| l2.next_table_mut(u64::from(page.l2_index()) as usize)) - .expect("mapping code does not support huge pages"); - let _frame = l3[u64::from(page.l3_index()) as usize]; - // .pointed_frame() - // .unwrap(); - l3[u64::from(page.l3_index()) as usize] = 0; /*.set_unused(); */ - // tlb::flush(VirtAddr(page.start_address())); - // TODO free p(1,2,3) table if empty - //allocator.deallocate_frame(frame); - // @todo do NOT deallocate frames either, but need to signal client that it's unused - } -} - -// Abstractions for page table entries. - -/// The error returned by the `PageTableEntry::frame` method. -#[derive(Snafu, Debug, Clone, Copy, PartialEq)] -pub enum FrameError { - /// The entry does not have the `PRESENT` flag set, so it isn't currently mapped to a frame. - FrameNotPresent, - /// The entry has the `HUGE_PAGE` flag set. The `frame` method has a standard 4KiB frame - /// as return type, so a huge frame can't be returned. @todo - HugeFrame, -} - -/// A 64-bit page table entry. -// pub struct PageTableEntry { -// entry: u64, -// } - -const ADDR_MASK: u64 = 0x0000_ffff_ffff_f000; -/* -impl PageTableEntry { - /// Creates an unused page table entry. - pub fn new() -> Self { - PageTableEntry::Invalid - } - - /// Returns whether this entry is zero. - pub fn is_unused(&self) -> bool { - self.entry == 0 - } - - /// Sets this entry to zero. - pub fn set_unused(&mut self) { - self.entry = 0; - } - - /// Returns the flags of this entry. - pub fn flags(&self) -> EntryFlags { - EntryFlags::new(self.entry) - } - - /// Returns the physical address mapped by this entry, might be zero. - pub fn addr(&self) -> PhysAddr { - PhysAddr::new(self.entry & ADDR_MASK) - } - - /// Returns the physical frame mapped by this entry. - /// - /// Returns the following errors: - /// - /// - `FrameError::FrameNotPresent` if the entry doesn't have the `PRESENT` flag set. - /// - `FrameError::HugeFrame` if the entry has the `HUGE_PAGE` flag set (for huge pages the - /// `addr` function must be used) - pub fn frame(&self) -> Result { - if !self.flags().read(STAGE1_DESCRIPTOR::VALID) { - Err(FrameError::FrameNotPresent) - // } else if self.flags().contains(EntryFlags::HUGE_PAGE) { - // Err(FrameError::HugeFrame) - } else { - Ok(PhysFrame::containing_address(self.addr())) - } - } - - /// Map the entry to the specified physical address with the specified flags. - pub fn set_addr(&mut self, addr: PhysAddr, flags: EntryFlags) { - assert!(addr.is_aligned(Size4KiB::SIZE)); - self.entry = addr.as_u64() | flags.bits(); - } - - /// Map the entry to the specified physical frame with the specified flags. - pub fn set_frame(&mut self, frame: PhysFrame, flags: EntryFlags) { - // assert!(!flags.contains(EntryFlags::HUGE_PAGE)); - self.set_addr(frame.start_address(), flags) - } - - /// Sets the flags of this entry. - pub fn set_flags(&mut self, flags: EntryFlags) { - // Todo: extract ADDR from self and replace all flags completely (?) - self.entry = self.addr().as_u64() | flags.bits(); - } -} - -impl fmt::Debug for PageTableEntry { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - let mut f = f.debug_struct("PageTableEntry"); - f.field("addr", &self.addr()); - f.field("flags", &self.flags()); - f.finish() - } -}*/ - -impl Table -where - Level: HierarchicalLevel, -{ - pub fn next_table_create( - &mut self, - index: usize, - allocator: &mut Alloc, - ) -> &mut Table - where - Alloc: FrameAllocator, - { - if self.next_table(index).is_none() { - assert!( - EntryRegister::new(self.entries[index]).read(STAGE1_DESCRIPTOR::TYPE) - == STAGE1_DESCRIPTOR::TYPE::Table.value, - "mapping code does not support huge pages" - ); - let frame = allocator.allocate_frame().expect("no frames available"); - self.entries[index] = PageTableEntry::TableDescriptor( - STAGE1_DESCRIPTOR::NEXT_LVL_TABLE_ADDR_4KiB.val(u64::from(frame)) - + STAGE1_DESCRIPTOR::VALID::True, - ) - .into(); - // self.entries[index] - // .set_frame(frame, STAGE1_DESCRIPTOR::VALID::True /*| WRITABLE*/); - self.next_table_mut(index).unwrap().zero(); - } - self.next_table_mut(index).unwrap() - } -} diff --git a/libs/memory/src/arch/aarch64/mod.rs b/libs/memory/src/arch/aarch64/mod.rs index a312c1844..7917b6168 100644 --- a/libs/memory/src/arch/aarch64/mod.rs +++ b/libs/memory/src/arch/aarch64/mod.rs @@ -5,27 +5,8 @@ //! Memory management functions for aarch64. -pub mod features; // @todo make only pub re-export? +pub mod features; pub mod mmu; -mod page_size; -mod phys_frame; -pub(crate) mod translation_table; -mod virt_page; +mod translation; -pub use phys_frame::PhysFrame; - -/// @todo ?? -pub trait FrameAllocator { - /// Allocate a physical memory frame. - fn allocate_frame(&mut self) -> Option; // @todo Result<> - /// Deallocate a physical frame. - fn deallocate_frame(&mut self, frame: PhysFrame); -} - -// Identity-map things for now. -// -// aarch64 granules and page sizes howto: -// https://stackoverflow.com/questions/34269185/simultaneous-existence-of-different-sized-pages-on-aarch64 - -/// Default page size used by the kernel. -pub const PAGE_SIZE: usize = 65536; +pub use translation::Aarch64_4K; diff --git a/libs/memory/src/arch/aarch64/page_size.rs b/libs/memory/src/arch/aarch64/page_size.rs deleted file mode 100644 index e62b455f9..000000000 --- a/libs/memory/src/arch/aarch64/page_size.rs +++ /dev/null @@ -1,68 +0,0 @@ -/// Trait for abstracting over the possible page sizes, 4KiB, 16KiB, 2MiB, 1GiB. -pub trait PageSize: Copy + PartialEq + Eq + PartialOrd + Ord { - /// The page size in bytes. - const SIZE: usize; - - /// A string representation of the page size for debug output. - const SIZE_AS_DEBUG_STR: &'static str; - - /// The page shift in bits. - const SHIFT: usize; - - /// The page mask in bits. - const MASK: u64; -} - -/// This trait is implemented for 4KiB, 16KiB, and 2MiB pages, but not for 1GiB pages. -pub trait NotGiantPageSize: PageSize {} // @todo doesn't have to be pub?? - -/// A standard 4KiB page. -#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)] -pub enum Size4KiB {} - -impl PageSize for Size4KiB { - const SIZE: usize = 4 * 1024; - const SIZE_AS_DEBUG_STR: &'static str = "4KiB"; - const SHIFT: usize = 12; - const MASK: u64 = 0xfff; -} - -impl NotGiantPageSize for Size4KiB {} - -/// A standard 16KiB page. -/// Currently unused. -#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)] -pub enum Size16KiB {} - -impl PageSize for Size16KiB { - const SIZE: usize = 16 * 1024; - const SIZE_AS_DEBUG_STR: &'static str = "16KiB"; - const SHIFT: usize = 14; - const MASK: u64 = 0x3fff; -} - -impl NotGiantPageSize for Size16KiB {} - -/// A “huge” 2MiB page. -#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)] -pub enum Size2MiB {} - -impl PageSize for Size2MiB { - const SIZE: usize = 2 * 1024 * 1024; - const SIZE_AS_DEBUG_STR: &'static str = "2MiB"; - const SHIFT: usize = 21; - const MASK: u64 = 0x1f_ffff; -} - -impl NotGiantPageSize for Size2MiB {} - -/// A “giant” 1GiB page. -#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)] -pub enum Size1GiB {} - -impl PageSize for Size1GiB { - const SIZE: usize = 1024 * 1024 * 1024; - const SIZE_AS_DEBUG_STR: &'static str = "1GiB"; - const SHIFT: usize = 30; - const MASK: u64 = 0x3fff_ffff; -} diff --git a/libs/memory/src/arch/aarch64/phys_frame.rs b/libs/memory/src/arch/aarch64/phys_frame.rs deleted file mode 100644 index b6c583516..000000000 --- a/libs/memory/src/arch/aarch64/phys_frame.rs +++ /dev/null @@ -1,200 +0,0 @@ -// Verbatim from https://github.com/rust-osdev/x86_64/blob/aa9ae54657beb87c2a491f2ab2140b2332afa6ba/src/structures/paging/frame.rs -// Abstractions for default-sized and huge physical memory frames. - -use { - super::page_size::{PageSize, Size4KiB}, - core::{ - fmt, - marker::PhantomData, - ops::{Add, AddAssign, Sub, SubAssign}, - }, - libaddress::PhysAddr, -}; - -/// A physical memory frame. -/// Frame is an addressable unit of the physical address space. -#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] -#[repr(C)] -pub struct PhysFrame { - start_address: PhysAddr, - size: PhantomData, -} - -impl From for PhysFrame { - fn from(address: u64) -> PhysFrame { - PhysFrame::containing_address(PhysAddr::new(address)) - } -} - -impl From> for u64 { - fn from(frame: PhysFrame) -> u64 { - frame.start_address.as_u64() - } -} - -impl PhysFrame { - /// Returns the frame that starts at the given virtual address. - /// - /// Returns an error if the address is not correctly aligned (i.e. is not a valid frame start). - pub fn from_start_address(address: PhysAddr) -> Result { - if !address.is_aligned(S::SIZE as u64) { - return Err(()); - } - Ok(PhysFrame::containing_address(address)) - } - - /// Returns the frame that contains the given physical address. - pub fn containing_address(address: PhysAddr) -> Self { - PhysFrame { - start_address: address.aligned_down(S::SIZE as u64), - size: PhantomData, - } - } - - /// Returns the start address of the frame. - pub fn start_address(&self) -> PhysAddr { - self.start_address - } - - /// Returns the size the frame (4KB, 2MB or 1GB). - pub fn size(&self) -> usize { - S::SIZE - } - - /// Returns a range of frames, exclusive `end`. - pub fn range(start: PhysFrame, end: PhysFrame) -> PhysFrameRange { - PhysFrameRange { start, end } - } - - /// Returns a range of frames, inclusive `end`. - pub fn range_inclusive(start: PhysFrame, end: PhysFrame) -> PhysFrameRangeInclusive { - PhysFrameRangeInclusive { start, end } - } -} - -impl fmt::Debug for PhysFrame { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - f.write_fmt(format_args!( - "PhysFrame[{}]({:#x})", - S::SIZE_AS_DEBUG_STR, - self.start_address().as_u64() - )) - } -} - -impl Add for PhysFrame { - type Output = Self; - /// Adds `rhs` same-sized frames to the current address. - fn add(self, rhs: u64) -> Self::Output { - PhysFrame::containing_address(self.start_address() + rhs * S::SIZE as u64) - } -} - -impl AddAssign for PhysFrame { - fn add_assign(&mut self, rhs: u64) { - *self = self.clone() + rhs; - } -} - -impl Sub for PhysFrame { - type Output = Self; - /// Subtracts `rhs` same-sized frames from the current address. - // @todo should I sub pages or just bytes here? - fn sub(self, rhs: u64) -> Self::Output { - PhysFrame::containing_address(self.start_address() - rhs * S::SIZE as u64) - } -} - -impl SubAssign for PhysFrame { - fn sub_assign(&mut self, rhs: u64) { - *self = self.clone() - rhs; - } -} - -impl Sub> for PhysFrame { - type Output = usize; - /// Return number of frames between start and end addresses. - fn sub(self, rhs: PhysFrame) -> Self::Output { - (self.start_address - rhs.start_address) as usize / S::SIZE - } -} - -/// A range of physical memory frames, exclusive the upper bound. -#[derive(Clone, Copy, PartialEq, Eq)] -#[repr(C)] -pub struct PhysFrameRange { - /// The start of the range, inclusive. - pub start: PhysFrame, - /// The end of the range, exclusive. - pub end: PhysFrame, -} - -impl PhysFrameRange { - /// Returns whether the range contains no frames. - pub fn is_empty(&self) -> bool { - !(self.start < self.end) - } -} - -impl Iterator for PhysFrameRange { - type Item = PhysFrame; - - fn next(&mut self) -> Option { - if !self.is_empty() { - let frame = self.start.clone(); - self.start += 1; - Some(frame) - } else { - None - } - } -} - -impl fmt::Debug for PhysFrameRange { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - f.debug_struct("PhysFrameRange") - .field("start", &self.start) - .field("end", &self.end) - .finish() - } -} - -/// An range of physical memory frames, inclusive the upper bound. -#[derive(Clone, Copy, PartialEq, Eq)] -#[repr(C)] -pub struct PhysFrameRangeInclusive { - /// The start of the range, inclusive. - pub start: PhysFrame, - /// The start of the range, exclusive. - pub end: PhysFrame, -} - -impl PhysFrameRangeInclusive { - /// Returns whether the range contains no frames. - pub fn is_empty(&self) -> bool { - !(self.start <= self.end) - } -} - -impl Iterator for PhysFrameRangeInclusive { - type Item = PhysFrame; - - fn next(&mut self) -> Option { - if !self.is_empty() { - let frame = self.start.clone(); - self.start += 1; - Some(frame) - } else { - None - } - } -} - -impl fmt::Debug for PhysFrameRangeInclusive { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - f.debug_struct("PhysFrameRangeInclusive") - .field("start", &self.start) - .field("end", &self.end) - .finish() - } -} diff --git a/libs/memory/src/arch/aarch64/translation.rs b/libs/memory/src/arch/aarch64/translation.rs new file mode 100644 index 000000000..ccd086205 --- /dev/null +++ b/libs/memory/src/arch/aarch64/translation.rs @@ -0,0 +1,276 @@ +use { + crate::arch_trait::{EntryKind, LevelCapabilities, TranslationArch}, + libaddress::{PhysAddr, VirtAddr}, + libmapping::{AccessPermissions, AttributeFields, MemAttributes}, +}; + +// --------------------------------------------------------------------------- +// AArch64 descriptor bit layout (Stage 1, 4KiB granule) +// --------------------------------------------------------------------------- +// +// With 4K granule, virtual address split: +// +// 63-48 47-39 38-30 29-21 20-12 11-0 +// signx L0 L1 L2 L3 off +// +// Each level indexes 512 entries (9 bits). Table size = 512 * 8 = 4096 bytes. +// +// L0: table pointer only +// L1: table pointer or 1GiB block +// L2: table pointer or 2MiB block +// L3: 4KiB page only (TYPE bit 1 = 1 for valid page) +// +// Descriptor formats (simplified): +// +// Table descriptor (L0/L1/L2): +// [63:48] upper attributes (ignored here) +// [47:12] next-level table address (4K aligned) +// [1] TYPE = 1 (table) +// [0] VALID = 1 +// +// Block descriptor (L1/L2): +// [63:48] upper attributes (UXN, PXN, ...) +// [47:N] output address (N=30 for L1 1GiB, N=21 for L2 2MiB) +// [11:2] lower attributes (AF, SH, AP, AttrIndx) +// [1] TYPE = 0 (block) +// [0] VALID = 1 +// +// Page descriptor (L3): +// [63:48] upper attributes +// [47:12] output address (4K aligned) +// [11:2] lower attributes +// [1] TYPE = 1 (page, note: different meaning than in table descriptor) +// [0] VALID = 1 + +// Descriptor bit positions +const VALID_BIT: u64 = 1 << 0; +const TYPE_BIT: u64 = 1 << 1; + +// Lower attribute bits +const ATTR_INDX_SHIFT: u64 = 2; +const AP_SHIFT: u64 = 6; +const SH_SHIFT: u64 = 8; +const AF_BIT: u64 = 1 << 10; + +// Upper attribute bits +const PXN_BIT: u64 = 1 << 53; +const UXN_BIT: u64 = 1 << 54; + +// Address masks (4K granule) +const TABLE_ADDR_MASK: u64 = 0x0000_FFFF_FFFF_F000; // [47:12] +const L1_BLOCK_ADDR_MASK: u64 = 0x0000_FFFF_C000_0000; // [47:30] +const L2_BLOCK_ADDR_MASK: u64 = 0x0000_FFFF_FFE0_0000; // [47:21] +const L3_PAGE_ADDR_MASK: u64 = 0x0000_FFFF_FFFF_F000; // [47:12] + +// AP field values +const AP_RW_EL1: u64 = 0b00 << AP_SHIFT; +const AP_RO_EL1: u64 = 0b10 << AP_SHIFT; + +// SH field values +const SH_INNER: u64 = 0b11 << SH_SHIFT; +const SH_OUTER: u64 = 0b10 << SH_SHIFT; + +/// MAIR_EL1 attribute indices, matching the MAIR setup in mmu.rs. +pub mod mair { + pub const NORMAL: u64 = 0; + pub const NORMAL_NON_CACHEABLE: u64 = 1; + pub const DEVICE_NGNRE: u64 = 2; +} + +// Block sizes +const SIZE_4K: usize = 4096; +const SIZE_2M: usize = 2 * 1024 * 1024; +const SIZE_1G: usize = 1024 * 1024 * 1024; + +/// AArch64 Stage 1 translation with 4KiB granule. +/// +/// 4-level hierarchy: L0 -> L1 -> L2 -> L3. +/// 512 entries per table, 4096-byte table size, 4096-byte alignment. +pub struct Aarch64_4K; + +impl TranslationArch for Aarch64_4K { + const NUM_LEVELS: usize = 4; + + fn entries_per_table(_level: usize) -> usize { + 512 // All levels have 512 entries with 4K granule + } + + fn table_alignment(_level: usize) -> usize { + SIZE_4K + } + + fn level_capabilities(level: usize) -> LevelCapabilities { + match level { + 0 => LevelCapabilities { + supports_table_pointer: true, + supports_block: false, + block_size: 0, + }, + 1 => LevelCapabilities { + supports_table_pointer: true, + supports_block: true, + block_size: SIZE_1G, + }, + 2 => LevelCapabilities { + supports_table_pointer: true, + supports_block: true, + block_size: SIZE_2M, + }, + 3 => LevelCapabilities { + supports_table_pointer: false, + supports_block: true, // "block" at L3 is a 4K page + block_size: SIZE_4K, + }, + _ => LevelCapabilities { + supports_table_pointer: false, + supports_block: false, + block_size: 0, + }, + } + } + + fn index_from_vaddr(vaddr: VirtAddr, level: usize) -> usize { + let shift = match level { + 0 => 39, + 1 => 30, + 2 => 21, + 3 => 12, + _ => return 0, + }; + ((vaddr.as_u64() >> shift) & 0x1FF) as usize + } + + fn decode_entry(raw: u64, level: usize) -> EntryKind { + if raw & VALID_BIT == 0 { + return EntryKind::Invalid; + } + + match level { + // L0: TYPE=1 means table, TYPE=0 is reserved/invalid + 0 => { + if raw & TYPE_BIT != 0 { + EntryKind::Table(PhysAddr::new(raw & TABLE_ADDR_MASK)) + } else { + EntryKind::Invalid + } + } + // L1: TYPE=1 means table, TYPE=0 means 1GiB block + 1 => { + if raw & TYPE_BIT != 0 { + EntryKind::Table(PhysAddr::new(raw & TABLE_ADDR_MASK)) + } else { + EntryKind::Block(PhysAddr::new(raw & L1_BLOCK_ADDR_MASK)) + } + } + // L2: TYPE=1 means table, TYPE=0 means 2MiB block + 2 => { + if raw & TYPE_BIT != 0 { + EntryKind::Table(PhysAddr::new(raw & TABLE_ADDR_MASK)) + } else { + EntryKind::Block(PhysAddr::new(raw & L2_BLOCK_ADDR_MASK)) + } + } + // L3: TYPE=1 means valid page, TYPE=0 is reserved/invalid + 3 => { + if raw & TYPE_BIT != 0 { + EntryKind::Block(PhysAddr::new(raw & L3_PAGE_ADDR_MASK)) + } else { + EntryKind::Invalid + } + } + _ => EntryKind::Invalid, + } + } + + fn encode_table_entry(next_table_phys: PhysAddr, _level: usize) -> u64 { + let addr = next_table_phys.as_u64(); + debug_assert!(addr & !TABLE_ADDR_MASK == 0, "Table address not 4K aligned"); + (addr & TABLE_ADDR_MASK) | TYPE_BIT | VALID_BIT + } + + fn encode_block_entry(phys: PhysAddr, attr: AttributeFields, level: usize) -> u64 { + let addr = phys.as_u64(); + let addr_mask = match level { + 1 => L1_BLOCK_ADDR_MASK, + 2 => L2_BLOCK_ADDR_MASK, + _ => panic!("Block entries only valid at L1 and L2"), + }; + debug_assert!( + addr & !addr_mask == 0, + "Block address not aligned for level" + ); + // TYPE=0 for block, VALID=1 + (addr & addr_mask) | encode_attributes(attr) | AF_BIT | VALID_BIT + } + + fn encode_page_entry(phys: PhysAddr, attr: AttributeFields) -> u64 { + let addr = phys.as_u64(); + debug_assert!( + addr & !L3_PAGE_ADDR_MASK == 0, + "Page address not 4K aligned" + ); + // TYPE=1 for page at L3, VALID=1 + (addr & L3_PAGE_ADDR_MASK) | encode_attributes(attr) | AF_BIT | TYPE_BIT | VALID_BIT + } + + fn output_address(raw: u64, level: usize) -> PhysAddr { + let mask = match level { + 0 => TABLE_ADDR_MASK, + 1 => { + if raw & TYPE_BIT != 0 { + TABLE_ADDR_MASK + } else { + L1_BLOCK_ADDR_MASK + } + } + 2 => { + if raw & TYPE_BIT != 0 { + TABLE_ADDR_MASK + } else { + L2_BLOCK_ADDR_MASK + } + } + 3 => L3_PAGE_ADDR_MASK, + _ => 0, + }; + PhysAddr::new(raw & mask) + } +} + +/// Encode `AttributeFields` into the lower+upper attribute bits of a +/// block/page descriptor. +fn encode_attributes(attr: AttributeFields) -> u64 { + let mut bits: u64 = 0; + + // Memory type -> MAIR index + shareability + match attr.mem_attributes { + MemAttributes::CacheableDRAM => { + bits |= mair::NORMAL << ATTR_INDX_SHIFT; + bits |= SH_INNER; + } + MemAttributes::NonCacheableDRAM => { + bits |= mair::NORMAL_NON_CACHEABLE << ATTR_INDX_SHIFT; + bits |= SH_INNER; + } + MemAttributes::Device => { + bits |= mair::DEVICE_NGNRE << ATTR_INDX_SHIFT; + bits |= SH_OUTER; + } + } + + // Access permissions + bits |= match attr.acc_perms { + AccessPermissions::ReadWrite => AP_RW_EL1, + AccessPermissions::ReadOnly => AP_RO_EL1, + }; + + // Execute-never + if attr.execute_never { + bits |= PXN_BIT; + } + + // Always set UXN until userspace is implemented + bits |= UXN_BIT; + + bits +} diff --git a/libs/memory/src/arch/aarch64/translation_table.rs b/libs/memory/src/arch/aarch64/translation_table.rs deleted file mode 100644 index 761dad8b2..000000000 --- a/libs/memory/src/arch/aarch64/translation_table.rs +++ /dev/null @@ -1,748 +0,0 @@ -use core::{ - marker::PhantomData, - ops::{Index, IndexMut}, -}; - -use { - crate::arch_mmu::{Granule64KiB, Granule512MiB, mair}, - core::convert, - libaddress::{Address, PhysAddr, Physical, Virtual}, - libmapping::{AccessPermissions, AttributeFields, MemAttributes, MemoryRegion, PageAddress}, - tock_registers::{ - interfaces::{Readable, Writeable}, - register_bitfields, - registers::InMemoryRegister, - }, -}; - -//-------------------------------------------------------------------------------------------------- -// Private Definitions -//-------------------------------------------------------------------------------------------------- - -register_bitfields! { - u64, - - /// A table descriptor, as per ARMv8-A Architecture Reference Manual Figure D5-15. - /// AArch64 Reference Manual page 2150, D5-2445 - pub STAGE1_TABLE_DESCRIPTOR [ - /// Physical address of the next descriptor. - NEXT_LEVEL_TABLE_ADDR_64KiB OFFSET(16) NUMBITS(32) [], // [47:16] - NEXT_LEVEL_TABLE_ADDR_4KiB OFFSET(12) NUMBITS(36) [], // [47:12] - - TYPE OFFSET(1) NUMBITS(1) [ - Block = 0, - Table = 1 - ], - - VALID OFFSET(0) NUMBITS(1) [ - False = 0, - True = 1 - ] - ] -} - -register_bitfields! { - u64, - - /// A level 3 page descriptor, as per ARMv8-A Architecture Reference Manual Figure D5-17. - /// AArch64 Reference Manual page 2150, D5-2445 - pub STAGE1_PAGE_DESCRIPTOR [ - /// Unprivileged execute-never. - UXN OFFSET(54) NUMBITS(1) [ - Execute = 0, - NeverExecute = 1 - ], - - /// Privileged execute-never - PXN OFFSET(53) NUMBITS(1) [ - Execute = 0, - NeverExecute = 1 - ], - - /// Physical address of the next table descriptor (lvl2) or the page descriptor (lvl3). - OUTPUT_ADDR_64KiB OFFSET(16) NUMBITS(32) [], // [47:16] - OUTPUT_ADDR_4KiB OFFSET(21) NUMBITS(27) [], // [47:21] - - /// Access flag - AF OFFSET(10) NUMBITS(1) [ - NotAccessed = 0, - Accessed = 1 - ], - - /// Shareability field - SH OFFSET(8) NUMBITS(2) [ - OuterShareable = 0b10, - InnerShareable = 0b11 - ], - - /// Access Permissions - AP OFFSET(6) NUMBITS(2) [ - RW_EL1 = 0b00, - RW_EL1_EL0 = 0b01, - RO_EL1 = 0b10, - RO_EL1_EL0 = 0b11 - ], - - /// Memory attributes index into the MAIR_EL1 register - AttrIndx OFFSET(2) NUMBITS(3) [], - - TYPE OFFSET(1) NUMBITS(1) [ - Reserved_Invalid = 0, - Page = 1 - ], - - VALID OFFSET(0) NUMBITS(1) [ - False = 0, - True = 1 - ] - ] -} - -/// A table descriptor with 64 KiB aperture. -/// -/// The output points to the next table. -#[derive(Copy, Clone)] -#[repr(C)] -pub struct TableDescriptor { - value: u64, -} - -/// A page descriptor with given aperture. -/// -/// The output points to physical memory. -#[derive(Copy, Clone)] -#[repr(C)] -pub struct PageDescriptor { - value: u64, -} - -pub trait BaseAddr { - fn phys_start_addr(&self) -> PhysAddr; - fn base_addr_u64(&self) -> u64; - fn base_addr_usize(&self) -> usize; -} - -// const NUM_LVL2_TABLES: usize = platform::memory::mmu::KernelAddrSpace::SIZE >> Granule512MiB::SHIFT; - -//-------------------------------------------------------------------------------------------------- -// Public Definitions -//-------------------------------------------------------------------------------------------------- - -/// Big monolithic struct for storing the translation tables. Individual levels must be 64 KiB -/// aligned, so the lvl3 is put first. -#[repr(C)] -#[repr(align(65536))] -pub struct FixedSizeTranslationTable { - /// Page descriptors, covering 64 KiB windows per entry. - lvl3: [[PageDescriptor; 8192]; NUM_TABLES], - - /// Table descriptors, covering 512 MiB windows. - lvl2: [TableDescriptor; NUM_TABLES], - - /// Have the tables been initialized? - initialized: bool, -} - -//-------------------------------------------------------------------------------------------------- -// Private Implementations -//-------------------------------------------------------------------------------------------------- - -impl BaseAddr for [T; N] { - // The binary is still identity mapped, so we don't need to convert here. - fn phys_start_addr(&self) -> PhysAddr { - Address::from_ptr(core::ptr::from_ref(self)) - } - - fn base_addr_u64(&self) -> u64 { - core::ptr::from_ref(self) as u64 - } - - fn base_addr_usize(&self) -> usize { - core::ptr::from_ref(self) as usize - } -} - -impl TableDescriptor { - /// Create an instance. - /// - /// Descriptor is invalid by default. - pub const fn zeroed() -> Self { - Self { value: 0 } - } - - /// Create an instance pointing to the supplied address. - pub fn from_next_lvl_table_addr(phys_next_lvl_table_addr: PhysAddr) -> Self { - let val = InMemoryRegister::::new(0); - - let shifted = phys_next_lvl_table_addr.as_usize() >> Granule64KiB::SHIFT; - val.write( - STAGE1_TABLE_DESCRIPTOR::NEXT_LEVEL_TABLE_ADDR_64KiB.val(shifted as u64) - + STAGE1_TABLE_DESCRIPTOR::TYPE::Table - + STAGE1_TABLE_DESCRIPTOR::VALID::True, - ); - - TableDescriptor { value: val.get() } - } -} - -impl PageDescriptor { - /// Create an instance. - /// - /// Descriptor is invalid by default. - pub const fn zeroed() -> Self { - Self { value: 0 } - } - - /// Create an instance. - pub fn from_output_page_addr( - phys_output_page_addr: PageAddress, - attribute_fields: AttributeFields, - ) -> Self { - let val = InMemoryRegister::::new(0); - - let shifted = phys_output_page_addr.into_inner().as_usize() / PAGE_SIZE; // FIXME: Granule::SHIFT - val.write( - STAGE1_PAGE_DESCRIPTOR::OUTPUT_ADDR_64KiB.val(shifted as u64) - + STAGE1_PAGE_DESCRIPTOR::AF::Accessed - + STAGE1_PAGE_DESCRIPTOR::TYPE::Page - + STAGE1_PAGE_DESCRIPTOR::VALID::True - + attribute_fields.into(), - ); - - Self { value: val.get() } - } - - /// Returns the valid bit. - fn is_valid(self) -> bool { - InMemoryRegister::::new(self.value) - .is_set(STAGE1_PAGE_DESCRIPTOR::VALID) - } -} - -/// Convert the kernel's generic memory attributes to HW-specific attributes of the MMU. -impl convert::From - for tock_registers::fields::FieldValue -{ - fn from(attribute_fields: AttributeFields) -> Self { - // Memory attributes - let mut desc = match attribute_fields.mem_attributes { - MemAttributes::CacheableDRAM => { - STAGE1_PAGE_DESCRIPTOR::SH::InnerShareable - + STAGE1_PAGE_DESCRIPTOR::AttrIndx.val(mair::attr::NORMAL) - } - MemAttributes::NonCacheableDRAM => { - STAGE1_PAGE_DESCRIPTOR::SH::InnerShareable - + STAGE1_PAGE_DESCRIPTOR::AttrIndx.val(mair::attr::NORMAL_NON_CACHEABLE) - } - MemAttributes::Device => { - STAGE1_PAGE_DESCRIPTOR::SH::OuterShareable - + STAGE1_PAGE_DESCRIPTOR::AttrIndx.val(mair::attr::DEVICE_NGNRE) - } - }; - - // Access Permissions - desc += match attribute_fields.acc_perms { - AccessPermissions::ReadOnly => STAGE1_PAGE_DESCRIPTOR::AP::RO_EL1, - AccessPermissions::ReadWrite => STAGE1_PAGE_DESCRIPTOR::AP::RW_EL1, - }; - - // The execute-never attribute is mapped to PXN in AArch64. - desc += if attribute_fields.execute_never { - STAGE1_PAGE_DESCRIPTOR::PXN::NeverExecute - } else { - STAGE1_PAGE_DESCRIPTOR::PXN::Execute - }; - - // Always set unprivileged execute-never as long as userspace is not implemented yet. - desc += STAGE1_PAGE_DESCRIPTOR::UXN::NeverExecute; - - desc - } -} - -//-------------------------------------------------------------------------------------------------- -// Public Code -//-------------------------------------------------------------------------------------------------- -use crate::{AddressSpace, AssociatedTranslationTable}; - -impl AssociatedTranslationTable for AddressSpace -where - [u8; Self::SIZE >> Granule512MiB::SHIFT]: Sized, -{ - type TableStartFromBottom = FixedSizeTranslationTable<{ Self::SIZE >> Granule512MiB::SHIFT }>; -} - -impl Default for FixedSizeTranslationTable { - fn default() -> Self { - Self::new() - } -} - -impl FixedSizeTranslationTable { - /// Create an instance. - #[allow(clippy::assertions_on_constants)] - pub const fn new() -> Self { - // assert!(libplatform::memory::KernelGranule::SIZE == Granule64KiB::SIZE); // assert! is const-fn-friendly - - // Can't have a zero-sized address space. - assert!(NUM_TABLES > 0); - - Self { - #[allow(clippy::large_stack_arrays)] - lvl3: [[PageDescriptor::zeroed(); 8192]; NUM_TABLES], - lvl2: [TableDescriptor::zeroed(); NUM_TABLES], - initialized: false, - } - } - - /// Helper to calculate the lvl2 and lvl3 indices from an address. - #[inline(always)] - fn lvl2_lvl3_index_from_page_addr( - virt_page_addr: PageAddress, - ) -> Result<(usize, usize), &'static str> { - let addr = virt_page_addr.into_inner().as_usize(); - let lvl2_index = addr >> Granule512MiB::SHIFT; - let lvl3_index = (addr & Granule512MiB::MASK) >> Granule64KiB::SHIFT; - - if lvl2_index >= NUM_TABLES { - return Err("Virtual page is out of bounds of translation table"); - } - - Ok((lvl2_index, lvl3_index)) - } - - /// Sets the `PageDescriptor` corresponding to the supplied page address. - /// - /// Doesn't allow overriding an already valid page. - #[inline(always)] - fn set_page_descriptor_from_page_addr( - &mut self, - virt_page_addr: PageAddress, - new_desc: PageDescriptor, - ) -> Result<(), &'static str> { - let (lvl2_index, lvl3_index) = Self::lvl2_lvl3_index_from_page_addr(virt_page_addr)?; - let desc = self - .lvl3 - .get_mut(lvl2_index) - .and_then(|lvl3_table: &mut [PageDescriptor; 8192]| lvl3_table.get_mut(lvl3_index)) - .ok_or("Invalid page address for translation table")?; - - if desc.is_valid() { - return Err("Virtual page is already mapped"); - } - - *desc = new_desc; - Ok(()) - } -} - -//------------------------------------------------------------------------------ -// OS Interface Code -//------------------------------------------------------------------------------ - -impl FixedSizeTranslationTable { - /// Iterates over all static translation table entries and fills them at once. - /// - /// See also: https://armv8-ref.codingbelief.com/en/chapter_d4/d4_the_aarch64_virtual_memory_system_archi.html - /// - /// # Safety - /// - /// - Modifies a `static mut`. Ensure it only happens from here. - pub unsafe fn populate_translation_table_entries(&mut self) -> Result<(), &'static str> { - // Point the first 1GiB to the level 2 table. - // LVL1_TABLE.entries[0] = - // PageTableEntry::new_table_descriptor(LVL2_TABLE.entries.base_addr_usize())?.into(); - // The remaining memory space (1GiB .. 2-8Gib) is yet unmapped. - - // Point the first 2 MiB of virtual addresses to the follow-up LVL3 - // page-table. - // LVL2_TABLE.entries[0] = - // PageTableEntry::new_table_descriptor(LVL3_TABLE.entries.base_addr_usize())?.into(); - - // Fill the rest of the LVL2 (2 MiB) entries as block descriptors. - // - // Notice the skip(1) which makes the iteration start at the second 2 MiB - // block (0x20_0000). - // for (block_descriptor_nr, entry) in LVL2_TABLE.entries.iter_mut().enumerate().skip(1) { - // let virt_addr = block_descriptor_nr << Size2MiB::SHIFT; - // let (output_addr, attribute_fields) = get_virt_addr_properties(virt_addr)?; - // - // println!( - // "Block Descriptor {:4} at {:#010x} -> {:#010x}, {}", - // block_descriptor_nr, virt_addr, output_addr, attribute_fields - // ); - // - // let block_desc = PageTableEntry::new_lvl2_block_descriptor(output_addr, attribute_fields)?; - // *entry = block_desc.into(); - // } - - // Finally, fill the single LVL3 table (4 KiB granule). - // for (page_descriptor_nr, entry) in LVL3_TABLE.entries.iter_mut().enumerate() { - // let virt_addr = page_descriptor_nr << Size4KiB::SHIFT; - // let (output_addr, attribute_fields) = get_virt_addr_properties(virt_addr)?; - // - // println!( - // "Page Descriptor {:4} at {:#010x} -> {:#010x}, {}", - // page_descriptor_nr, virt_addr, output_addr, attribute_fields - // ); - // - // let page_desc = PageTableEntry::new_page_descriptor(output_addr, attribute_fields)?; - // *entry = page_desc.into(); - // } - - // for (l2_nr, l2_entry) in self.lvl2.iter_mut().enumerate() { - // *l2_entry = - // TableDescriptor::from_next_lvl_table_addr(self.lvl3[l2_nr].base_addr_usize()); - - // for (l3_nr, l3_entry) in self.lvl3[l2_nr].iter_mut().enumerate() { - // let virt_addr = (l2_nr << Granule512MiB::SHIFT) + (l3_nr << Granule64KiB::SHIFT); - - // let (phys_output_addr, attribute_fields) = - // platform::memory::mmu::unused::virt_mem_layout() - // .virt_addr_properties(virt_addr)?; - - // *l3_entry = PageDescriptor::from_output_addr(phys_output_addr, &attribute_fields); - // } - // } - - Ok(()) - } -} - -impl crate::TranslationTable for FixedSizeTranslationTable { - fn init(&mut self) -> Result<(), &'static str> { - if self.initialized { - return Ok(()); - } - - // Populate the l2 entries. - for (lvl2_nr, lvl2_entry) in self.lvl2.iter_mut().enumerate() { - let phys_table_addr = self - .lvl3 - .get(lvl2_nr) - .ok_or("Invalid lvl2 index")? - .phys_start_addr(); - - let new_desc = TableDescriptor::from_next_lvl_table_addr(phys_table_addr); - *lvl2_entry = new_desc; - } - - self.initialized = true; - Ok(()) - } - - fn phys_base_address(&self) -> PhysAddr { - self.lvl2.phys_start_addr() - } - - unsafe fn map_at( - &mut self, - virt_region: &MemoryRegion, - phys_region: &MemoryRegion, - attr: AttributeFields, - ) -> Result<(), &'static str> { - assert!(self.initialized, "Translation tables not initialized"); - - if virt_region.size() != phys_region.size() { - return Err("Tried to map memory regions with different sizes"); - } - - // TODO: keep track of phys memory end - // if phys_region.end_exclusive_page_addr() - // > crate::platform::memory::phys_addr_space_end_exclusive_addr() - // { - // return Err("Tried to map outside of physical address space"); - // } - - #[allow(clippy::useless_conversion)] - let iter = phys_region.into_iter().zip(virt_region.into_iter()); - for (phys_page_addr, virt_page_addr) in iter { - let new_desc = PageDescriptor::from_output_page_addr(phys_page_addr, attr); - let virt_page = virt_page_addr; - - self.set_page_descriptor_from_page_addr(virt_page, new_desc)?; - } - - Ok(()) - } -} - -//-------------------------------------------------------------------------------------------------- -// wait: my extended code -//-------------------------------------------------------------------------------------------------- - -/* - * With 4k page granule, a virtual address is split into 4 lookup parts - * spanning 9 bits each: - * - * _______________________________________________ - * | | | | | | | - * | signx | Lv0 | Lv1 | Lv2 | Lv3 | off | - * |_______|_______|_______|_______|_______|_______| - * 63-48 47-39 38-30 29-21 20-12 11-00 - * - * mask page size - * - * Lv0: FF8000000000 -- - * Lv1: 7FC0000000 1G - * Lv2: 3FE00000 2M - * Lv3: 1FF000 4K - * off: FFF - * - * RPi3 supports 64K and 4K granules, also 40-bit physical addresses. - * It also can address only 1G physical memory, so these 40-bit phys addresses are a fake. - * - * 48-bit virtual address space; different mappings in VBAR0 (EL0) and VBAR1 (EL1+). - */ - -/// Number of entries in a 4KiB mmu table. -pub const NUM_ENTRIES_4KIB: u64 = 512; - -/// Trait for abstracting over the possible page sizes, 4KiB, 16KiB, 2MiB, 1GiB. -pub trait PageSize: Copy + Eq + PartialOrd + Ord { - /// The page size in bytes. - const SIZE: u64; - - /// A string representation of the page size for debug output. - const SIZE_AS_DEBUG_STR: &'static str; - - /// The page shift in bits. - const SHIFT: usize; - - /// The page mask in bits. - const MASK: u64; -} - -/// This trait is implemented for 4KiB, 16KiB, and 2MiB pages, but not for 1GiB pages. -pub trait NotGiantPageSize: PageSize {} // @todo doesn't have to be pub?? - -/// A standard 4KiB page. -#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] -pub enum Size4KiB {} - -impl PageSize for Size4KiB { - const SIZE: u64 = 4096; - const SIZE_AS_DEBUG_STR: &'static str = "4KiB"; - const SHIFT: usize = 12; - const MASK: u64 = 0xfff; -} - -impl NotGiantPageSize for Size4KiB {} - -/// A “huge” 2MiB page. -#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] -pub enum Size2MiB {} - -impl PageSize for Size2MiB { - const SIZE: u64 = Size4KiB::SIZE * NUM_ENTRIES_4KIB; - const SIZE_AS_DEBUG_STR: &'static str = "2MiB"; - const SHIFT: usize = 21; - const MASK: u64 = 0x1fffff; -} - -impl NotGiantPageSize for Size2MiB {} - -pub type PageFlags = tock_registers::fields::FieldValue; -pub type TableFlags = tock_registers::fields::FieldValue; -// type EntryRegister = register::LocalRegisterCopy; - -/// L0 table -- only pointers to L1 tables -pub enum PageGlobalDirectory {} -/// L1 tables -- pointers to L2 tables or giant 1GiB pages -pub enum PageUpperDirectory {} -/// L2 tables -- pointers to L3 tables or huge 2MiB pages -pub enum PageDirectory {} -/// L3 tables -- only pointers to 4/16KiB pages -pub enum PageTable {} - -/// Shared trait for specific table levels. -pub trait TableLevel {} - -/// Shared trait for hierarchical table levels. -/// -/// Specifies what is the next level of page table hierarchy. -pub trait HierarchicalLevel: TableLevel { - /// Level of the next translation table below this one. - type NextLevel: TableLevel; -} - -impl TableLevel for PageGlobalDirectory {} -impl TableLevel for PageUpperDirectory {} -impl TableLevel for PageDirectory {} -impl TableLevel for PageTable {} - -impl HierarchicalLevel for PageGlobalDirectory { - type NextLevel = PageUpperDirectory; -} -impl HierarchicalLevel for PageUpperDirectory { - type NextLevel = PageDirectory; -} -impl HierarchicalLevel for PageDirectory { - type NextLevel = PageTable; -} -// PageTables do not have next level, therefore they are not HierarchicalLevel - -/// MMU address translation table. -/// Contains just u64 internally, provides enum interface on top -#[repr(C)] -#[repr(align(4096))] -pub struct Table { - entries: [u64; NUM_ENTRIES_4KIB as usize], - level: PhantomData, -} - -// Implementation code shared for all levels of page tables -impl Table -where - L: TableLevel, -{ - /// Zero out entire table. - pub fn zero(&mut self) { - for entry in self.entries.iter_mut() { - *entry = 0; - } - } -} - -impl Index for Table -where - L: TableLevel, -{ - type Output = u64; - - fn index(&self, index: usize) -> &u64 { - &self.entries[index] - } -} - -impl IndexMut for Table -where - L: TableLevel, -{ - fn index_mut(&mut self, index: usize) -> &mut u64 { - &mut self.entries[index] - } -} - -/// Type-safe enum wrapper covering Table's 64-bit entries. -#[derive(Clone)] -// #[repr(transparent)] -enum PageTableEntry { - /// Empty page table entry. - Invalid, - /// Table descriptor is a L0, L1 or L2 table pointing to another table. - /// L0 tables can only point to L1 tables. - /// A descriptor pointing to the next page table. - TableDescriptor(TableFlags), - /// A Level2 block descriptor with 2 MiB aperture. - /// - /// The output points to physical memory. - Lvl2BlockDescriptor(TableFlags), - /// A page PageTableEntry::descriptor with 4 KiB aperture. - /// - /// The output points to physical memory. - PageDescriptor(PageFlags), -} - -// A descriptor pointing to the next page table. (within PageTableEntry enum) -// struct TableDescriptor(register::FieldValue); - -impl PageTableEntry { - fn new_table_descriptor(next_lvl_table_addr: usize) -> Result { - if next_lvl_table_addr % Size4KiB::SIZE as usize != 0 { - // @todo SIZE must be usize - return Err("TableDescriptor: Address is not 4 KiB aligned."); - } - - let shifted = next_lvl_table_addr >> Size4KiB::SHIFT; - - Ok(PageTableEntry::TableDescriptor( - STAGE1_TABLE_DESCRIPTOR::VALID::True - + STAGE1_TABLE_DESCRIPTOR::TYPE::Table - + STAGE1_TABLE_DESCRIPTOR::NEXT_LEVEL_TABLE_ADDR_4KiB.val(shifted as u64), - )) - } -} - -// A Level2 block descriptor with 2 MiB aperture. -// -// The output points to physical memory. -// struct Lvl2BlockDescriptor(register::FieldValue); - -impl PageTableEntry { - fn new_lvl2_block_descriptor( - output_addr: usize, - _attribute_fields: AttributeFields, - ) -> Result { - if output_addr % Size2MiB::SIZE as usize != 0 { - return Err("BlockDescriptor: Address is not 2 MiB aligned."); - } - - let shifted = output_addr >> Size2MiB::SHIFT; - - Ok(PageTableEntry::Lvl2BlockDescriptor( - STAGE1_TABLE_DESCRIPTOR::VALID::True - + STAGE1_TABLE_DESCRIPTOR::TYPE::Block - + STAGE1_TABLE_DESCRIPTOR::NEXT_LEVEL_TABLE_ADDR_4KiB.val(shifted as u64), // + attribute_fields.into(), - )) - } -} - -// A page descriptor with 4 KiB aperture. -// -// The output points to physical memory. - -impl PageTableEntry { - fn new_page_descriptor( - output_addr: usize, - _attribute_fields: AttributeFields, - ) -> Result { - if output_addr % Size4KiB::SIZE as usize != 0 { - return Err("PageDescriptor: Address is not 4 KiB aligned."); - } - - let shifted = output_addr >> Size4KiB::SHIFT; - - Ok(PageTableEntry::PageDescriptor( - STAGE1_PAGE_DESCRIPTOR::VALID::True - // + STAGE1_PAGE_DESCRIPTOR::AF::Accessed - + STAGE1_PAGE_DESCRIPTOR::TYPE::Page - + STAGE1_PAGE_DESCRIPTOR::OUTPUT_ADDR_4KiB.val(shifted as u64), // + attribute_fields.into(), - )) - } -} - -impl From for PageTableEntry { - fn from(_val: u64) -> PageTableEntry { - // xx00 -> Invalid - // xx10 -> Block Entry in L1 and L2 - // xx11 -> TableDescriptor in L0, L1 and L2 - // xx11 -> PageDescriptor in L3 - PageTableEntry::Invalid - } -} - -impl From for u64 { - fn from(val: PageTableEntry) -> u64 { - match val { - PageTableEntry::Invalid => 0, - PageTableEntry::TableDescriptor(x) | PageTableEntry::Lvl2BlockDescriptor(x) => x.value, - PageTableEntry::PageDescriptor(x) => x.value, - } - } -} - -static mut LVL1_TABLE: Table = Table:: { - entries: [0; NUM_ENTRIES_4KIB as usize], - level: PhantomData, -}; - -static mut LVL2_TABLE: Table = Table:: { - entries: [0; NUM_ENTRIES_4KIB as usize], - level: PhantomData, -}; - -static mut LVL3_TABLE: Table = Table:: { - entries: [0; NUM_ENTRIES_4KIB as usize], - level: PhantomData, -}; diff --git a/libs/memory/src/arch/aarch64/virt_page.rs b/libs/memory/src/arch/aarch64/virt_page.rs deleted file mode 100644 index 9f62690be..000000000 --- a/libs/memory/src/arch/aarch64/virt_page.rs +++ /dev/null @@ -1,334 +0,0 @@ -// Verbatim from https://github.com/rust-osdev/x86_64/blob/aa9ae54657beb87c2a491f2ab2140b2332afa6ba/src/structures/paging/page.rs -// Abstractions for default-sized and huge virtual memory pages. - -// @fixme x86_64 page level numbering: P4 -> P3 -> P2 -> P1 -// @fixme armv8a page level numbering: L0 -> L1 -> L2 -> L3 - -#![allow(dead_code)] - -use { - crate::arch::aarch64::page_size::{NotGiantPageSize, PageSize, Size1GiB, Size2MiB, Size4KiB}, - core::{ - fmt, - marker::PhantomData, - ops::{Add, AddAssign, Sub, SubAssign}, - }, - libaddress::VirtAddr, - ux::u9, -}; - -/// A virtual memory page. -#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] -pub struct Page { - start_address: VirtAddr, - size: PhantomData, -} - -pub enum Error { - NotAligned, -} - -impl Page { - /// The page size in bytes. - pub const SIZE: usize = S::SIZE; - - /// Returns the page that starts at the given virtual address. - /// - /// Returns an error if the address is not correctly aligned (i.e. is not a valid page start). - pub fn from_start_address(address: VirtAddr) -> Result { - if !address.is_aligned(S::SIZE as u64) { - Err(Error::NotAligned) - } else { - Ok(Page::containing_address(address)) - } - } - - /// Returns the page that contains the given virtual address. - pub fn containing_address(address: VirtAddr) -> Self { - Page { - start_address: address.aligned_down(S::SIZE as u64), - size: PhantomData, - } - } - - /// Returns the start address of the page. - pub fn start_address(&self) -> VirtAddr { - self.start_address - } - - /// Returns the size the page (4KB, 2MB or 1GB). - pub const fn size(&self) -> usize { - S::SIZE - } - - /// Returns the level 0 page table index of this page. - pub fn l0_index(&self) -> u9 { - self.start_address().l0_index() - } - - /// Returns the level 1 page table index of this page. - pub fn l1_index(&self) -> u9 { - self.start_address().l1_index() - } - - /// Returns a range of pages, exclusive `end`. - pub fn range(start: Self, end: Self) -> PageRange { - PageRange { start, end } - } - - /// Returns a range of pages, inclusive `end`. - pub fn range_inclusive(start: Self, end: Self) -> PageRangeInclusive { - PageRangeInclusive { start, end } - } -} - -impl Page { - /// Returns the level 2 page table index of this page. - pub fn l2_index(&self) -> u9 { - self.start_address().l2_index() - } -} - -impl Page { - /// Returns the 1GiB memory page with the specified page table indices. - pub fn from_page_table_indices_1gib(l0_index: u9, l1_index: u9) -> Self { - use bit_field::BitField; - - let mut addr = 0; - addr.set_bits(39..48, u64::from(l0_index)); - addr.set_bits(30..39, u64::from(l1_index)); - Page::containing_address(VirtAddr::new(addr)) - } -} - -impl Page { - /// Returns the 2MiB memory page with the specified page table indices. - pub fn from_page_table_indices_2mib(l0_index: u9, l1_index: u9, l2_index: u9) -> Self { - use bit_field::BitField; - - let mut addr = 0; - addr.set_bits(39..48, u64::from(l0_index)); - addr.set_bits(30..39, u64::from(l1_index)); - addr.set_bits(21..30, u64::from(l2_index)); - Page::containing_address(VirtAddr::new(addr)) - } -} - -impl Page { - /// Returns the 4KiB memory page with the specified page table indices. - pub fn from_page_table_indices(l0_index: u9, l1_index: u9, l2_index: u9, l3_index: u9) -> Self { - use bit_field::BitField; - - let mut addr = 0; - addr.set_bits(39..48, u64::from(l0_index)); - addr.set_bits(30..39, u64::from(l1_index)); - addr.set_bits(21..30, u64::from(l2_index)); - addr.set_bits(12..21, u64::from(l3_index)); - Page::containing_address(VirtAddr::new(addr)) - } - - /// Returns the level 3 page table index of this page. - pub fn l3_index(&self) -> u9 { - self.start_address().l3_index() - } -} - -impl fmt::Debug for Page { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - f.write_fmt(format_args!( - "Page<{}>({:#x})", - S::SIZE_AS_DEBUG_STR, - self.start_address().as_u64() - )) - } -} - -impl Add for Page { - type Output = Self; - /// Add a number of bytes to the current page and find the next page with the same size - /// containing the resulting address. - fn add(self, rhs: T) -> Self::Output { - Page::containing_address(self.start_address() + rhs) - } -} - -impl AddAssign for Page { - fn add_assign(&mut self, rhs: T) { - *self = *self + rhs; - } -} - -impl Sub for Page { - type Output = Self; - /// Subtract a number of bytes from the current page start address and find - /// the next page with the same size containing the resulting address. - fn sub(self, rhs: T) -> Self::Output { - Page::containing_address(self.start_address() - rhs) - } -} - -impl SubAssign for Page { - fn sub_assign(&mut self, rhs: T) { - *self = *self - rhs; - } -} - -impl Sub for Page { - type Output = usize; // @todo must be isize here? - /// Return number of bytes between two pages' starting addresses. - fn sub(self, rhs: Self) -> Self::Output { - (self.start_address - rhs.start_address) as usize - } -} - -/// A range of pages with exclusive upper bound. -#[derive(Clone, Copy, PartialEq, Eq)] -pub struct PageRange { - /// The start of the range, inclusive. - pub start: Page, - /// The end of the range, exclusive. - pub end: Page, -} - -impl PageRange { - /// Returns whether this range contains no pages. - pub fn is_empty(&self) -> bool { - self.start >= self.end - } - - pub fn num_pages(&self) -> usize { - (self.end - self.start) / S::SIZE - } -} - -impl Iterator for PageRange { - type Item = Page; - - fn next(&mut self) -> Option { - if !self.is_empty() { - let page = self.start; - self.start += S::SIZE; // @todo Destructive iterator - Some(page) - } else { - None - } - } -} - -impl PageRange { - /// Converts the range of 4KiB pages to a range of 4KiB pages (identity operation). - #[inline(always)] - pub fn as_4kib_page_range(&self) -> PageRange { - *self - } -} - -impl PageRange { - /// Converts the range of 2MiB pages to a range of 4KiB pages. - // @todo what about range of 1GiB pages? - pub fn as_4kib_page_range(&self) -> PageRange { - PageRange { - start: Page::containing_address(self.start.start_address()), - // @fixme end is calculated incorrectly, add test - end: Page::containing_address(self.end.start_address()), - } - } -} - -impl fmt::Debug for PageRange { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - f.debug_struct("PageRange") - .field("start", &self.start) - .field("end", &self.end) - .finish() - } -} - -/// A range of pages with inclusive upper bound. -#[derive(Clone, Copy, PartialEq, Eq)] -pub struct PageRangeInclusive { - /// The start of the range, inclusive. - pub start: Page, - /// The end of the range, inclusive. - pub end: Page, -} - -impl PageRangeInclusive { - /// Returns whether this range contains no pages. - pub fn is_empty(&self) -> bool { - self.start > self.end - } -} - -impl Iterator for PageRangeInclusive { - type Item = Page; - - fn next(&mut self) -> Option { - if !self.is_empty() { - let page = self.start; - self.start += S::SIZE; - Some(page) - } else { - None - } - } -} - -impl fmt::Debug for PageRangeInclusive { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - f.debug_struct("PageRangeInclusive") - .field("start", &self.start) - .field("end", &self.end) - .finish() - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test_case] - pub fn test_page_ranges() { - let page_size = Size4KiB::SIZE; - let number = 1000_usize; - - let start_addr = VirtAddr::new(0xdeafbead); - let start: Page = Page::::containing_address(start_addr); - let end = start + number * page_size; - - let mut range = Page::range(start, end); - for i in 0..number { - assert_eq!( - range.next(), - Some(Page::containing_address(start_addr + page_size * i)) - ); - } - assert_eq!(range.next(), None); - - let mut range_inclusive = Page::range_inclusive(start, end); - for i in 0..=number { - assert_eq!( - range_inclusive.next(), - Some(Page::containing_address(start_addr + page_size * i)) - ); - } - assert_eq!(range_inclusive.next(), None); - } - - #[test_case] - fn test_page_range_conversion() { - let page_size = Size2MiB::SIZE; - let number = 10; - - let start_addr = VirtAddr::new(0xdeadbeaf); - let start = Page::::containing_address(start_addr); - let end = start + number * page_size; - - let range = Page::::range(start, end); - assert_eq!(range.num_pages(), 10); - - let range = range.as_4kib_page_range(); - // 10 2MiB pages is 5120 4KiB pages - assert_eq!(range.num_pages(), 5120); - } -} diff --git a/libs/memory/src/arch/mod.rs b/libs/memory/src/arch/mod.rs index 3fd4b4298..06905431f 100644 --- a/libs/memory/src/arch/mod.rs +++ b/libs/memory/src/arch/mod.rs @@ -1,5 +1,2 @@ #[cfg(target_arch = "aarch64")] pub mod aarch64; - -#[cfg(target_arch = "aarch64")] -pub use aarch64::*; diff --git a/libs/memory/src/arch_trait.rs b/libs/memory/src/arch_trait.rs new file mode 100644 index 000000000..4282b518e --- /dev/null +++ b/libs/memory/src/arch_trait.rs @@ -0,0 +1,72 @@ +use { + libaddress::{PhysAddr, VirtAddr}, + libmapping::AttributeFields, +}; + +/// What kinds of entries a given table level supports. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct LevelCapabilities { + /// This level can contain pointers to the next-level table. + pub supports_table_pointer: bool, + /// This level can contain block/page mappings to physical memory. + pub supports_block: bool, + /// If block mappings are supported, the block size in bytes. + /// For the leaf level (e.g. L3 with 4K pages), this is the page size. + pub block_size: usize, +} + +/// A decoded translation table entry. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum EntryKind { + /// Entry is invalid / not present. + Invalid, + /// Entry points to the next-level translation table at the given physical address. + Table(PhysAddr), + /// Entry maps a block (or page) of physical memory. + Block(PhysAddr), +} + +/// Architecture-specific translation table format. +/// +/// Implementations of this trait encode all the knowledge about a specific +/// MMU translation scheme: how many levels, how entries are encoded, what +/// block sizes each level supports, etc. +/// +/// The trait uses only associated functions (no `&self`) so implementations +/// are zero-sized type tags (e.g. `struct Aarch64_4K;`). +pub trait TranslationArch { + /// Number of levels in the translation hierarchy (e.g. 4 for AArch64 4K granule). + const NUM_LEVELS: usize; + + /// Number of entries in a table at the given level. + fn entries_per_table(level: usize) -> usize; + + /// Required alignment in bytes for a table at the given level. + fn table_alignment(level: usize) -> usize; + + /// Size in bytes of a table at the given level (entries * 8). + fn table_size(level: usize) -> usize { + Self::entries_per_table(level) * core::mem::size_of::() + } + + /// What kinds of entries this level supports. + fn level_capabilities(level: usize) -> LevelCapabilities; + + /// Extract the table index from a virtual address for a given level. + fn index_from_vaddr(vaddr: VirtAddr, level: usize) -> usize; + + /// Decode a raw 64-bit entry at a given level into its semantic meaning. + fn decode_entry(raw: u64, level: usize) -> EntryKind; + + /// Encode a table pointer entry (pointing to the next-level table). + fn encode_table_entry(next_table_phys: PhysAddr, level: usize) -> u64; + + /// Encode a block mapping entry at levels that support blocks (L1 1G, L2 2M). + fn encode_block_entry(phys: PhysAddr, attr: AttributeFields, level: usize) -> u64; + + /// Encode a page mapping entry at the leaf level (L3 for 4K granule). + fn encode_page_entry(phys: PhysAddr, attr: AttributeFields) -> u64; + + /// Extract the output physical address from a raw entry at a given level. + fn output_address(raw: u64, level: usize) -> PhysAddr; +} diff --git a/libs/memory/src/error.rs b/libs/memory/src/error.rs new file mode 100644 index 000000000..cfd505d84 --- /dev/null +++ b/libs/memory/src/error.rs @@ -0,0 +1,27 @@ +use snafu::Snafu; + +/// Errors from translation table operations. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Snafu)] +pub enum TableError { + /// Entry index is out of bounds for this table level. + #[snafu(display("entry index out of bounds"))] + IndexOutOfBounds, + /// The provided memory slice has wrong size for the given table level. + #[snafu(display("memory slice has wrong size for table level"))] + InvalidTableSize, + /// The table level is not valid for this architecture. + #[snafu(display("invalid table level for this architecture"))] + InvalidLevel, + /// This table level does not support block mappings. + #[snafu(display("block mappings not supported at this level"))] + BlockNotSupported, + /// This table level does not support table pointers. + #[snafu(display("table pointers not supported at this level"))] + TablePointerNotSupported, + /// The provided physical address is not properly aligned for this entry type. + #[snafu(display("physical address not properly aligned"))] + MisalignedAddress, + /// Attempted to overwrite a valid entry without clearing it first. + #[snafu(display("entry is already valid"))] + EntryAlreadyValid, +} diff --git a/libs/memory/src/lib.rs b/libs/memory/src/lib.rs index 846294400..7f83b5877 100644 --- a/libs/memory/src/lib.rs +++ b/libs/memory/src/lib.rs @@ -3,142 +3,35 @@ * Copyright (c) Berkus Decker */ -//! The arch-independent representation of a MMU and memory translation tables. - -// To test we need to impl this for x86_64, aarch64 and riscv64 arches -// and provide the same interface from the arch-independent layer. - -// Need to be able to -// a) create page table hierarchy at different granule size and addressing mode (user/kernel) -// b) inspect/walk table hierarchy and resolve virtual-to-physical addresses via provided tables -// c) modify/invalidate page hierarchy descriptors +//! Translation table management for the Vesper nanokernel. +//! +//! This library provides arch-independent abstractions for MMU translation +//! tables. Each table level is a first-class object, matching the kernel's +//! capability-based syscall API where GlobalDirectory, PageDirectory, Frame +//! etc. are separate capabilities. +//! +//! All table memory is externally provided — this library never allocates. #![no_std] -#![allow(dead_code)] // while refactoring -#![allow(incomplete_features)] -#![feature(generic_const_exprs)] // incomplete_features -#![feature(const_trait_impl)] -#![feature(format_args_nl)] #![allow(internal_features)] -#![feature(allocator_api)] -#![feature(core_intrinsics)] -#![feature(step_trait)] -#![feature(custom_test_frameworks)] - -use { - libaddress::{Address, Physical}, - snafu::Snafu, - translation_table::interface::TranslationTable, -}; +#![feature(core_intrinsics)] // internal feature +#![feature(format_args_nl)] -pub use crate::arch::mmu as arch_mmu; +pub mod arch_trait; +pub mod error; +pub mod table; +pub mod walk; mod arch; -pub mod page_alloc; -pub mod translation_table; - -//-------------------------------------------------------------------------------------------------- -// Architectural Public Reexports -//-------------------------------------------------------------------------------------------------- -// pub use arch_mmu::*; - -//-------------------------------------------------------------------------------------------------- -// Public Definitions -//-------------------------------------------------------------------------------------------------- - -/// MMU enable errors variants. -// @todo rework error types -#[allow(missing_docs)] -#[derive(Debug, Snafu)] -pub enum MMUEnableError { - #[snafu(display("MMU is already enabled"))] - AlreadyEnabled, - #[snafu(display("{}", err))] - Other { err: &'static str }, -} - -/// Memory Management interfaces. -pub mod interface { - use super::*; - - /// MMU functions. - pub trait MMU { - /// Turns on the MMU for the first time and enables data and instruction caching. - /// - /// # Safety - /// - /// - Changes the hardware's global state. - unsafe fn enable_mmu_and_caching( - &self, - phys_tables_base_addr: Address, - ) -> Result<(), MMUEnableError>; - - /// Returns true if the MMU is enabled, false otherwise. - fn is_enabled(&self) -> bool; - - fn print_features(&self); // debug - } -} - -/// Describes the characteristics of a translation granule. -pub struct TranslationGranule; -/// Describes properties of an address space. -pub struct AddressSpace; - -/// Intended to be implemented for [`AddressSpace`]. -pub trait AssociatedTranslationTable { - /// A translation table whose address range is: - /// - /// [`AS_SIZE` - 1, 0] - type TableStartFromBottom; -} - -//-------------------------------------------------------------------------------------------------- -// Private Code -//-------------------------------------------------------------------------------------------------- - -// Query the platform for the reserved virtual addresses for MMIO remapping -// and initialize the kernel's MMIO VA allocator with it. -// fn kernel_init_mmio_va_allocator() { -// let region = platform::memory::mmu::virt_mmio_remap_region(); -// page_alloc::kernel_mmio_va_allocator().lock(|allocator| allocator.init(region)); -// } - -//-------------------------------------------------------------------------------------------------- -// Public Code -//-------------------------------------------------------------------------------------------------- - -impl TranslationGranule { - /// The granule's size. - pub const SIZE: usize = Self::size_checked(); - - /// The granule's mask. - pub const MASK: usize = Self::SIZE - 1; - - /// The granule's shift, aka log2(size). - pub const SHIFT: usize = Self::SIZE.trailing_zeros() as usize; - - const fn size_checked() -> usize { - assert!(GRANULE_SIZE.is_power_of_two()); - - GRANULE_SIZE - } -} - -impl AddressSpace { - /// The address space size. - pub const SIZE: usize = Self::size_checked(); - - /// The address space shift, aka log2(size). - pub const SIZE_SHIFT: usize = Self::SIZE.trailing_zeros() as usize; - - const fn size_checked() -> usize { - assert!(AS_SIZE.is_power_of_two()); - - // Check for architectural restrictions as well. - // Self::arch_address_space_size_sanity_checks(); +// Re-export core types at crate root. +pub use { + arch_trait::{EntryKind, LevelCapabilities, TranslationArch}, + error::TableError, + table::{Table, TableRef}, + walk::{TranslationResult, translate}, +}; - AS_SIZE - } -} +// Re-export arch implementations. +#[cfg(target_arch = "aarch64")] +pub use arch::aarch64::{Aarch64_4K, features, mmu}; diff --git a/libs/memory/src/page_alloc.rs b/libs/memory/src/page_alloc.rs deleted file mode 100644 index 2fe35bb06..000000000 --- a/libs/memory/src/page_alloc.rs +++ /dev/null @@ -1,77 +0,0 @@ -// SPDX-License-Identifier: MIT OR Apache-2.0 -// -// Copyright (c) 2021-2022 Andre Richter - -//! Page allocation. - -use { - super::MemoryRegion, - core::num::NonZeroUsize, - libaddress::{AddressType, Virtual}, - liblocking::IRQSafeNullLock, - liblog::warn, -}; - -//-------------------------------------------------------------------------------------------------- -// Public Definitions -//-------------------------------------------------------------------------------------------------- - -/// A page allocator that can be lazyily initialized. -pub struct PageAllocator { - pool: Option>, -} - -//-------------------------------------------------------------------------------------------------- -// Global instances -//-------------------------------------------------------------------------------------------------- - -// TODO: drop this, kernel should not be allocating any memory!! -static KERNEL_MMIO_VA_ALLOCATOR: IRQSafeNullLock> = - IRQSafeNullLock::new(PageAllocator::::new()); - -//-------------------------------------------------------------------------------------------------- -// Public Code -//-------------------------------------------------------------------------------------------------- - -/// Return a reference to the kernel's MMIO virtual address allocator. -pub fn kernel_mmio_va_allocator() -> &'static IRQSafeNullLock> { - &KERNEL_MMIO_VA_ALLOCATOR -} - -impl Default for PageAllocator { - fn default() -> Self { - Self::new() - } -} - -impl PageAllocator { - /// Create an instance. - pub const fn new() -> Self { - Self { pool: None } - } - - /// Initialize the allocator. - pub fn init(&mut self, pool: MemoryRegion) { - if self.pool.is_some() { - warn!("Already initialized"); - return; - } - - self.pool = Some(pool); - } - - /// Allocate a number of pages. - pub fn alloc( - &mut self, - num_requested_pages: NonZeroUsize, - ) -> Result, &'static str> { - if self.pool.is_none() { - return Err("Allocator not initialized"); - } - - self.pool - .as_mut() - .unwrap() - .take_first_n_pages(num_requested_pages) - } -} diff --git a/libs/memory/src/table.rs b/libs/memory/src/table.rs new file mode 100644 index 000000000..fd0580d44 --- /dev/null +++ b/libs/memory/src/table.rs @@ -0,0 +1,240 @@ +use { + crate::{ + arch_trait::{EntryKind, LevelCapabilities, TranslationArch}, + error::TableError, + }, + core::marker::PhantomData, + libaddress::PhysAddr, + libmapping::AttributeFields, +}; + +/// A single translation table at a specific level. +/// +/// Wraps an externally-provided `&mut [u64]` slice — memory is never +/// allocated internally. The kernel provides table memory through its +/// capability system. +/// +/// The level is stored as a runtime value so that all table capabilities +/// can be stored uniformly in the capability system without needing +/// separate types per level. +pub struct Table<'a, A: TranslationArch> { + entries: &'a mut [u64], + level: usize, + _arch: PhantomData, +} + +/// An immutable view of a translation table. +pub struct TableRef<'a, A: TranslationArch> { + entries: &'a [u64], + level: usize, + _arch: PhantomData, +} + +impl<'a, A: TranslationArch> Table<'a, A> { + /// Create a new table from a zeroed memory region. + /// + /// The caller must ensure: + /// - `memory` points to a correctly-sized, correctly-aligned region + /// - `memory` is zeroed + /// - `level` is valid for the architecture (0..NUM_LEVELS) + pub fn from_memory(memory: &'a mut [u64], level: usize) -> Result { + if level >= A::NUM_LEVELS { + return Err(TableError::InvalidLevel); + } + let expected = A::entries_per_table(level); + if memory.len() != expected { + return Err(TableError::InvalidTableSize); + } + Ok(Self { + entries: memory, + level, + _arch: PhantomData, + }) + } + + /// Wrap an existing populated table. + /// + /// Same requirements as `from_memory` except the memory need not be zeroed. + pub fn from_existing(memory: &'a mut [u64], level: usize) -> Result { + Self::from_memory(memory, level) + } + + /// The table level (0 = root, NUM_LEVELS-1 = leaf). + pub fn level(&self) -> usize { + self.level + } + + /// Number of entries in this table. + pub fn num_entries(&self) -> usize { + self.entries.len() + } + + /// What this level supports. + pub fn capabilities(&self) -> LevelCapabilities { + A::level_capabilities(self.level) + } + + /// Read the raw u64 value at the given index. + pub fn read_raw(&self, index: usize) -> Result { + self.entries + .get(index) + .copied() + .ok_or(TableError::IndexOutOfBounds) + } + + /// Read and decode the entry at the given index. + pub fn read_entry(&self, index: usize) -> Result { + let raw = self.read_raw(index)?; + Ok(A::decode_entry(raw, self.level)) + } + + /// Write a table pointer entry at the given index. + /// + /// The entry must currently be invalid (clear it first if overwriting). + pub fn set_table_entry( + &mut self, + index: usize, + next_table_phys: PhysAddr, + ) -> Result<(), TableError> { + if !A::level_capabilities(self.level).supports_table_pointer { + return Err(TableError::TablePointerNotSupported); + } + let slot = self + .entries + .get_mut(index) + .ok_or(TableError::IndexOutOfBounds)?; + if A::decode_entry(*slot, self.level) != EntryKind::Invalid { + return Err(TableError::EntryAlreadyValid); + } + *slot = A::encode_table_entry(next_table_phys, self.level); + Ok(()) + } + + /// Write a block (or page at leaf level) mapping entry. + /// + /// The entry must currently be invalid. + pub fn set_block_entry( + &mut self, + index: usize, + phys: PhysAddr, + attr: AttributeFields, + ) -> Result<(), TableError> { + let caps = A::level_capabilities(self.level); + if !caps.supports_block { + return Err(TableError::BlockNotSupported); + } + let slot = self + .entries + .get_mut(index) + .ok_or(TableError::IndexOutOfBounds)?; + if A::decode_entry(*slot, self.level) != EntryKind::Invalid { + return Err(TableError::EntryAlreadyValid); + } + // Use page encoding at the leaf level, block encoding otherwise. + *slot = if self.level == A::NUM_LEVELS - 1 { + A::encode_page_entry(phys, attr) + } else { + A::encode_block_entry(phys, attr, self.level) + }; + Ok(()) + } + + /// Clear (invalidate) the entry at the given index. + pub fn clear_entry(&mut self, index: usize) -> Result<(), TableError> { + let slot = self + .entries + .get_mut(index) + .ok_or(TableError::IndexOutOfBounds)?; + *slot = 0; + Ok(()) + } + + /// Write a raw u64 value at the given index. + /// + /// This bypasses all validation — use only when you know exactly + /// what descriptor bits to set. + /// + /// # Safety + /// The caller must ensure the raw value is a valid descriptor for this level. + pub unsafe fn write_raw(&mut self, index: usize, value: u64) -> Result<(), TableError> { + let slot = self + .entries + .get_mut(index) + .ok_or(TableError::IndexOutOfBounds)?; + *slot = value; + Ok(()) + } + + /// Iterate over all entries, yielding (index, decoded entry) pairs. + pub fn iter(&self) -> impl Iterator + '_ { + let level = self.level; + self.entries + .iter() + .enumerate() + .map(move |(i, &raw)| (i, A::decode_entry(raw, level))) + } + + /// Iterate over only valid (non-invalid) entries. + pub fn iter_valid(&self) -> impl Iterator + '_ { + self.iter().filter(|(_, kind)| *kind != EntryKind::Invalid) + } + + /// Get an immutable view of this table. + pub fn as_ref(&self) -> TableRef<'_, A> { + TableRef { + entries: self.entries, + level: self.level, + _arch: PhantomData, + } + } +} + +impl<'a, A: TranslationArch> TableRef<'a, A> { + /// Create a read-only view from a slice. + pub fn from_slice(entries: &'a [u64], level: usize) -> Result { + if level >= A::NUM_LEVELS { + return Err(TableError::InvalidLevel); + } + let expected = A::entries_per_table(level); + if entries.len() != expected { + return Err(TableError::InvalidTableSize); + } + Ok(Self { + entries, + level, + _arch: PhantomData, + }) + } + + pub fn level(&self) -> usize { + self.level + } + + pub fn num_entries(&self) -> usize { + self.entries.len() + } + + pub fn read_raw(&self, index: usize) -> Result { + self.entries + .get(index) + .copied() + .ok_or(TableError::IndexOutOfBounds) + } + + pub fn read_entry(&self, index: usize) -> Result { + let raw = self.read_raw(index)?; + Ok(A::decode_entry(raw, self.level)) + } + + pub fn iter(&self) -> impl Iterator + '_ { + let level = self.level; + self.entries + .iter() + .enumerate() + .map(move |(i, &raw)| (i, A::decode_entry(raw, level))) + } + + pub fn iter_valid(&self) -> impl Iterator + '_ { + self.iter().filter(|(_, kind)| *kind != EntryKind::Invalid) + } +} diff --git a/libs/memory/src/translation_table.rs b/libs/memory/src/translation_table.rs deleted file mode 100644 index 07c10db6a..000000000 --- a/libs/memory/src/translation_table.rs +++ /dev/null @@ -1,51 +0,0 @@ -//! Translation table interface. -// TODO: Move to libmmu, or libmapping - higher level mapping interface. - -use crate::arch::translation_table as arch_translation_table; - -use { - libaddress::{Address, Physical, Virtual}, - libmapping::{AttributeFields, MemoryRegion}, -}; - -//-------------------------------------------------------------------------------------------------- -// Architectural Public Reexports -//-------------------------------------------------------------------------------------------------- -pub use arch_translation_table::FixedSizeTranslationTable; - -//-------------------------------------------------------------------------------------------------- -// Public Definitions -//-------------------------------------------------------------------------------------------------- - -/// Translation table interfaces. -pub mod interface { - use super::*; - - /// Translation table operations. - pub trait TranslationTable { - /// Anything that needs to run before any of the other provided functions can be used. - /// - /// - Implementor must ensure that this function can run only once or is harmless if invoked - /// multiple times. - fn init(&mut self) -> Result<(), &'static str>; - - /// The translation table's base address to be used for programming the MMU. - fn phys_base_address(&self) -> Address; - - /// Map the given virtual memory region to the given physical memory region. - /// - /// # Safety - /// - /// - Using wrong attributes can cause multiple issues of different nature in the system. - /// - It is not required that the architectural implementation prevents aliasing. That is, - /// mapping to the same physical memory using multiple virtual addresses, which would - /// break Rust's ownership assumptions. This should be protected against in the kernel's - /// generic MMU code. - unsafe fn map_at( - &mut self, - virt_region: &MemoryRegion, - phys_region: &MemoryRegion, - attr: AttributeFields, - ) -> Result<(), &'static str>; - } -} diff --git a/libs/memory/src/walk.rs b/libs/memory/src/walk.rs new file mode 100644 index 000000000..9250c5664 --- /dev/null +++ b/libs/memory/src/walk.rs @@ -0,0 +1,63 @@ +use { + crate::arch_trait::{EntryKind, TranslationArch}, + libaddress::{PhysAddr, VirtAddr}, +}; + +/// Result of a successful address translation walk. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct TranslationResult { + /// The resolved physical address. + pub phys_addr: PhysAddr, + /// The level at which the translation terminated (block or page). + pub level: usize, + /// The block/page size at the terminating level. + pub block_size: usize, +} + +/// Walk the translation table hierarchy to translate a virtual address. +/// +/// The `resolve` callback converts a physical table address and level into +/// a slice of u64 entries. This keeps the library independent of any +/// physical-to-virtual mapping strategy — the caller decides how to +/// access physical memory (identity map, kernel window, etc). +/// +/// Returns `None` if the walk encounters an invalid entry at any level. +pub fn translate( + root_phys: PhysAddr, + vaddr: VirtAddr, + resolve: F, +) -> Option +where + A: TranslationArch, + F: Fn(PhysAddr, usize) -> Option<&'static [u64]>, +{ + let mut table_phys = root_phys; + + for level in 0..A::NUM_LEVELS { + let entries = resolve(table_phys, level)?; + let index = A::index_from_vaddr(vaddr, level); + let raw = *entries.get(index)?; + + match A::decode_entry(raw, level) { + EntryKind::Invalid => return None, + EntryKind::Table(next_phys) => { + table_phys = next_phys; + // Continue to next level. + } + EntryKind::Block(block_phys) => { + let caps = A::level_capabilities(level); + let offset = vaddr.as_u64() as usize & (caps.block_size - 1); + let phys_addr = PhysAddr::new(block_phys.as_u64() + offset as u64); + return Some(TranslationResult { + phys_addr, + level, + block_size: caps.block_size, + }); + } + } + } + + // Reached past the last level without finding a block/page — should not happen + // with a well-formed architecture, but handle gracefully. + None +} diff --git a/libs/mmio/Cargo.toml b/libs/mmio/Cargo.toml index 18a6e73f1..3a4732dcb 100644 --- a/libs/mmio/Cargo.toml +++ b/libs/mmio/Cargo.toml @@ -16,6 +16,7 @@ publish = false maintenance = { status = "experimental" } [dependencies] +libaddress = { workspace = true } safe-mmio = { workspace = true } # Tests are offloaded to kernel integration tests binary. diff --git a/libs/object/Cargo.toml b/libs/object/Cargo.toml index d9bbbb501..88199553e 100644 --- a/libs/object/Cargo.toml +++ b/libs/object/Cargo.toml @@ -20,7 +20,7 @@ maintenance = { status = "experimental" } [dependencies] libaddress = { workspace = true } -libmemory = { workspace = true } +# libmemory = { workspace = true } libprint = { workspace = true } libqemu = { workspace = true } libsyscall = { workspace = true } diff --git a/libs/platform/Cargo.toml b/libs/platform/Cargo.toml index 448aeaf04..5fb9cc625 100644 --- a/libs/platform/Cargo.toml +++ b/libs/platform/Cargo.toml @@ -42,6 +42,7 @@ liblocal-irq = { workspace = true } liblocking = { workspace = true } liblog = { workspace = true } libmapping = { workspace = true } +libmmio = { workspace = true } libprimitives = { workspace = true } libtime = { workspace = true } once_cell = { workspace = true } diff --git a/libs/platform/src/raspberrypi/device_driver/bcm/gpio.rs b/libs/platform/src/raspberrypi/device_driver/bcm/gpio.rs index 951085e50..2753f4088 100644 --- a/libs/platform/src/raspberrypi/device_driver/bcm/gpio.rs +++ b/libs/platform/src/raspberrypi/device_driver/bcm/gpio.rs @@ -6,10 +6,11 @@ */ use { - crate::device_driver::{IRQNumber, common::MMIODerefWrapper}, + crate::device_driver::IRQNumber, core::marker::PhantomData, libaddress::{Address, Virtual}, liblocking::{IRQSafeNullLock, interface::Mutex}, + libmmio::MMIODerefWrapper, tock_registers::{ fields::FieldValue, interfaces::{ReadWriteable, Readable, Writeable}, diff --git a/libs/platform/src/raspberrypi/device_driver/bcm/interrupt_controller/peripheral_ic.rs b/libs/platform/src/raspberrypi/device_driver/bcm/interrupt_controller/peripheral_ic.rs index c78a00542..e2892bc4d 100644 --- a/libs/platform/src/raspberrypi/device_driver/bcm/interrupt_controller/peripheral_ic.rs +++ b/libs/platform/src/raspberrypi/device_driver/bcm/interrupt_controller/peripheral_ic.rs @@ -10,9 +10,9 @@ use { super::{PendingIRQs, PeripheralIRQ}, - crate::device_driver::common::MMIODerefWrapper, libexception::asynchronous::{IRQContext, IRQHandlerDescriptor, interface::IRQManager}, liblocking::{self, IRQSafeNullLock, InitStateLock}, + libmmio::MMIODerefWrapper, tock_registers::{ interfaces::{Readable, Writeable}, register_structs, diff --git a/libs/platform/src/raspberrypi/device_driver/bcm/mailbox.rs b/libs/platform/src/raspberrypi/device_driver/bcm/mailbox.rs index 6f48521ca..a281db514 100644 --- a/libs/platform/src/raspberrypi/device_driver/bcm/mailbox.rs +++ b/libs/platform/src/raspberrypi/device_driver/bcm/mailbox.rs @@ -12,7 +12,7 @@ #![allow(dead_code)] use { - crate::{BcmHost, device_driver::common::MMIODerefWrapper}, + crate::BcmHost, aarch64_cpu::asm::barrier, core::{ // alloc::{AllocError, Allocator, Layout}, @@ -21,6 +21,7 @@ use { result::Result as CoreResult, sync::atomic::{Ordering, compiler_fence}, }, + libmmio::MMIODerefWrapper, // liblocking::IRQSafeNullLock, // libaddress::{Address, Virtual}, snafu::Snafu, @@ -150,7 +151,7 @@ impl MailboxStorage for LocalMailboxStorage { impl MailboxStorage for DmaBackedMailboxStorage { fn new() -> Result { - use libmemory::platform::memory::map::virt::DMA_HEAP_START; + use crate::memory::map::virt::DMA_HEAP_START; Ok(Self { storage: DMA_HEAP_START diff --git a/libs/platform/src/raspberrypi/device_driver/bcm/mini_uart.rs b/libs/platform/src/raspberrypi/device_driver/bcm/mini_uart.rs index cceaf3003..7c92f0878 100644 --- a/libs/platform/src/raspberrypi/device_driver/bcm/mini_uart.rs +++ b/libs/platform/src/raspberrypi/device_driver/bcm/mini_uart.rs @@ -8,11 +8,7 @@ // #[cfg(not(feature = "noserial"))] // use tock_registers::interfaces::{Readable, Writeable}; use { - crate::{ - BcmHost, - device_driver::{common::MMIODerefWrapper, gpio}, - exception::asynchronous::IRQNumber, - }, + crate::{BcmHost, device_driver::gpio, exception::asynchronous::IRQNumber}, core::{ convert::From, fmt::{self, Arguments}, @@ -20,6 +16,7 @@ use { libaddress::{Address, Virtual}, libconsole::{SerialOps, console::interface}, liblocking::{IRQSafeNullLock, interface::Mutex}, + libmmio::MMIODerefWrapper, tock_registers::{ interfaces::ReadWriteable, register_bitfields, register_structs, diff --git a/libs/platform/src/raspberrypi/device_driver/bcm/pl011_uart.rs b/libs/platform/src/raspberrypi/device_driver/bcm/pl011_uart.rs index e97a0c334..79e2d5855 100644 --- a/libs/platform/src/raspberrypi/device_driver/bcm/pl011_uart.rs +++ b/libs/platform/src/raspberrypi/device_driver/bcm/pl011_uart.rs @@ -9,11 +9,12 @@ */ use { - crate::device_driver::{IRQNumber, common::MMIODerefWrapper, gpio}, + crate::device_driver::{IRQNumber, gpio}, core::fmt::{self, Arguments}, libaddress::{Address, Virtual}, libconsole::{SerialOps, console::interface}, liblocking::{IRQSafeNullLock, interface::Mutex}, + libmmio::MMIODerefWrapper, libprimitives::cpu::loop_while, // snafu::Snafu, tock_registers::{ diff --git a/libs/platform/src/raspberrypi/device_driver/bcm/power.rs b/libs/platform/src/raspberrypi/device_driver/bcm/power.rs index 84be2bb6e..e70797369 100644 --- a/libs/platform/src/raspberrypi/device_driver/bcm/power.rs +++ b/libs/platform/src/raspberrypi/device_driver/bcm/power.rs @@ -7,11 +7,11 @@ use { crate::device_driver::{ - common::MMIODerefWrapper, gpio, mailbox::{Mailbox, MailboxOps, channel}, }, libaddress::{Address, Virtual}, + libmmio::MMIODerefWrapper, snafu::Snafu, tock_registers::{ interfaces::{Readable, Writeable}, diff --git a/libs/platform/src/raspberrypi/drivers.rs b/libs/platform/src/raspberrypi/drivers.rs index 5cfae0cd9..7e1450d08 100644 --- a/libs/platform/src/raspberrypi/drivers.rs +++ b/libs/platform/src/raspberrypi/drivers.rs @@ -1,6 +1,6 @@ use { super::exception, // @todo - crate::{device_driver, exception::asynchronous::IRQNumber}, + crate::{device_driver, exception::asynchronous::IRQNumber, memory::map::mmio}, core::{ mem::MaybeUninit, sync::atomic::{AtomicBool, Ordering}, @@ -173,12 +173,12 @@ unsafe fn instantiate_interrupt_controller() -> Result<(), &'static str> { let gic_distr_mmio_descriptor = MMIODescriptor::new(mmio::GICD_BASE, mmio::GICD_SIZE); let gic_distr_virt_addr = // SAFETY: Not safe! - unsafe { libmemory::mmu::kernel_map_mmio("GICv2 GICD", &gic_distr_mmio_descriptor)? }; + unsafe { libmapping::kernel_map_mmio("GICv2 GICD", &gic_distr_mmio_descriptor)? }; let gic_ctrlr_mmio_descriptor = MMIODescriptor::new(mmio::GICC_BASE, mmio::GICC_SIZE); let gic_ctrlr_virt_addr = // SAFETY: Not safe! - unsafe { libmemory::mmu::kernel_map_mmio("GICV2 GICC", &gic_ctrlr_mmio_descriptor)? }; + unsafe { libmapping::kernel_map_mmio("GICV2 GICC", &gic_ctrlr_mmio_descriptor)? }; #[allow(static_mut_refs)] // SAFETY: Not safe! diff --git a/libs/platform/src/raspberrypi/memory.rs b/libs/platform/src/raspberrypi/memory.rs index 1d5588eb9..64cc27429 100644 --- a/libs/platform/src/raspberrypi/memory.rs +++ b/libs/platform/src/raspberrypi/memory.rs @@ -7,30 +7,29 @@ //! use { - crate::mmu::{ - AddressSpace, AssociatedTranslationTable, MemoryRegion, PageAddress, TranslationGranule, - }, libaddress::PhysAddr, + // libmapping::{MemoryRegion, PageAddress}, + // libmemory::{AddressSpace, AssociatedTranslationTable, TranslationGranule}, }; //-------------------------------------------------------------------------------------------------- // Private Definitions //-------------------------------------------------------------------------------------------------- -type KernelTranslationTable = - ::TableStartFromBottom; +// type KernelTranslationTable = +// ::TableStartFromBottom; //-------------------------------------------------------------------------------------------------- // Public Definitions //-------------------------------------------------------------------------------------------------- -/// The translation granule chosen by this platform. This will be used everywhere else -/// in the kernel to derive respective data structures and their sizes. -/// For example, the `crate::memory::mmu::Page`. -pub type KernelGranule = TranslationGranule<{ 64 * 1024 }>; +// The translation granule chosen by this platform. This will be used everywhere else +// in the kernel to derive respective data structures and their sizes. +// For example, the `crate::memory::mmu::Page`. +// pub type KernelGranule = TranslationGranule<{ 64 * 1024 }>; -/// The kernel's virtual address space defined by this platform. -pub type KernelVirtAddrSpace = AddressSpace<{ 1024 * 1024 * 1024 }>; +// The kernel's virtual address space defined by this platform. +// pub type KernelVirtAddrSpace = AddressSpace<{ 1024 * 1024 * 1024 }>; /// The board's physical memory map. /// This is a fixed memory map for Raspberry Pi, diff --git a/play/memtab.rs b/play/memtab.rs deleted file mode 100644 index 99da956e8..000000000 --- a/play/memtab.rs +++ /dev/null @@ -1,520 +0,0 @@ -//# snafu = "*" -// Explore memory table abstractions -// -// -#[allow(unused_imports)] -use { - core::{ - marker::PhantomData, - ops::{Index, IndexMut}, - }, - snafu::Snafu, -}; - -struct FrameAllocator { - counter: usize; -} - -impl FrameAllocator { - fn new() -> Self { - Self {counter:0} - } - // Return a frame allocated from physical memory. - // Frame physical address is mapped to kernel space. - fn grab_frame(&mut self) -> G::Frame { - let phys_base = self.allocate_phys_frame(); - Frame::new(phys_base.phys_to_kernel()) - } - - fn allocate_phys_frame(&mut self) -> usize { - self.counter += 1; - self.counter * G::Size - } -} - -struct Frame(usize); - -impl Frame { - pub fn new(base: usize) -> Self { - Self(base) - } -} - -fn main() { - println!("Hello, play"); - - // Build page table hierarchy from Stage1 down to Stage4. - let allocator = FrameAllocator::new(); - - // Using Granule4k: - let l0_table = 0; - - // Using Granule16k: - let l0_table = 0; - - // Using Granule64k: - let l0_table = Stage1::::new(); - let arena = allocator.grab_frame::(); // give parent table here, to extract granule - let l1_table = l0_table.allocate_page_from(arena); - let arena = allocator.grab_frame::(); - let l2_table = l1_table.allocate_page_from(arena); - - //================= - // TRAVERSE TABLES - //================= - - let base_addr = 124467000usize; // this comes from TTBRx register or some table base variable for each process. - - // everything starts with a translation table base address - let sys_l0_table = base_addr as *const Stage1; - - // Access the table using virtual addresses (each stage consumes more bits of the address). - let l0_table = sys_l0_table; - let l1 = l0_table[virt_addr]; - let l2 = l1_table[virt_addr]; - let phys = l2_table[virt_addr]; - - // to access physical memory from kernel - let phys = 123456usize; - let kern_phys = phys.phys_to_kernel(); -} - -trait PhysicalKernelMapping { - type PhysAddr; - fn phys_to_kernel(&self) -> Self::PhysAddr; - fn kernel_to_phys(&self) -> Self::PhysAddr; -} - -const KERNEL_PHYS_MAP_BASE: usize = 0xffff_fff0_0000_0000; // Not const, but defined by available RAM size - -impl PhysicalKernelMapping for usize { - type PhysAddr = usize; - #[inline(always)] - fn phys_to_kernel(&self) -> usize { - self.checked_add(KERNEL_PHYS_MAP_BASE) - .expect("Physical to kernel mapping overflowed") - } - #[inline(always)] - fn kernel_to_phys(&self) -> usize { - self.checked_sub(KERNEL_PHYS_MAP_BASE) - .expect("Kernel to physical mapping underflowed") - } -} - -// impl indexing stages by virtual address only, -// so l0[virt][virt][virt][virt] will go through all levels of translation -// - -impl Index for Stage1 { - type Output = Self::NextStage; - - fn index(&self, index: Virtual) -> Self::NextStage {} -} - -type Physical = u64; -type Virtual = u64; - -trait Stage { - type G: Granule; - type NextStage; -} - -trait Descriptor { - type GRANULE: Granule; - type STAGE: Stage; // Get NextStage from this STAGE - fn is_leaf() -> bool; -} - -trait NextLevelDescriptor: Descriptor { - fn get_next_level_desciptor() -> impl Descriptor; -} - -trait LeafDescriptor: Descriptor { - fn get_translated_address() -> Physical; -} - -/// Abstract over possible granule sizes. -trait Granule { - // Mask and shift to extract entry index from virt address - const MASK_BITS: usize; - const MASK: u64 = 1 << Self::MASK_BITS - 1; - const SHIFT: usize; - fn get_index(address: Virtual) -> usize { - return ((address >> Self::SHIFT) & Self::MASK) - .try_into() - .expect("Arithmetics gone mad"); - } -} - -/// Abstract over the possible page sizes, 4KiB, 16KiB, 2MiB, 1GiB. -/// ... .arch independent page sizes actually, so not linked to granules or anything... -/// Page sizes depend on stage and granule size - could be auto-derived? -pub trait PageSize: Copy + PartialEq + Eq + PartialOrd + Ord { - /// A string representation of the page size for debug output. - const SIZE_AS_DEBUG_STR: &'static str; - - /// The page shift in bits. - const SHIFT: usize; - - /// The page size in bytes. - const SIZE: usize = 1 << Self::SHIFT; - - /// The page size mask in bits. - const MASK: u64 = 1 << Self::SHIFT - 1; - - fn alignment() -> usize { - Self::SIZE - } - - fn mask() -> u64 { - Self::MASK - } -} - -/// This trait is implemented for 4KiB, 16KiB, and 2MiB pages, but not for 1GiB pages. -/// This trait is actually not necessary - do the BlockSize trait instead. -trait NotGiantPageSize: PageSize {} - -/// Marker for granule sizes impls. -trait GranuleSize {} - -/// Stages are parameterised by the used granule size. This determines their max size and resolution step. -/// It also determines the return type (table/block/page) of the resolution step? hmm. -/// (use an assoc type to resolve it e.g. trait NextStage { type Resolution; fn resolve() -> Self::Resolution; }) -struct Stage1 { - _granule: PhantomData, -} -struct Stage2 { - _granule: PhantomData, -} -struct Stage3 { - _granule: PhantomData, -} -struct Stage4 { - _granule: PhantomData, -} - -impl Stage1 { - pub fn new() -> Self { - Self { - _granule: PhantomData, - } - } -} - -trait NextStage { - type BaseTable; // not Stage1..4 b/c we can't pass any stage to a stage 1 - type Resolution; - fn resolve(_table: &Self::BaseTable) -> Self::Resolution; -} - -// @todo: impl_granule! macro? -impl NextStage for Stage1 { - type BaseTable = u64; // ? - type Resolution = u64; // ? - fn resolve(_table: &Self::BaseTable) -> Self::Resolution { - 0u64 - } -} -impl NextStage for Stage1 { - type BaseTable = u64; // ? - type Resolution = u64; // ? - fn resolve(_table: &Self::BaseTable) -> Self::Resolution { - 0u64 - } -} -impl NextStage for Stage1 { - type BaseTable = u64; // ? - type Resolution = u64; // ? - fn resolve(_table: &Self::BaseTable) -> Self::Resolution { - 0u64 - } -} - -impl NextStage for Stage2 { - type BaseTable = u64; // ? - type Resolution = u64; // ? - fn resolve(_table: &Self::BaseTable) -> Self::Resolution { - 0u64 - } -} -impl NextStage for Stage2 { - type BaseTable = u64; // ? - type Resolution = u64; // ? - fn resolve(_table: &Self::BaseTable) -> Self::Resolution { - 0u64 - } -} -impl NextStage for Stage2 { - type BaseTable = u64; // ? - type Resolution = u64; // ? - fn resolve(_table: &Self::BaseTable) -> Self::Resolution { - 0u64 - } -} - -impl NextStage for Stage3 { - type BaseTable = u64; // ? - type Resolution = u64; // ? - fn resolve(_table: &Self::BaseTable) -> Self::Resolution { - 0u64 - } -} -impl NextStage for Stage3 { - type BaseTable = u64; // ? - type Resolution = u64; // ? - fn resolve(_table: &Self::BaseTable) -> Self::Resolution { - 0u64 - } -} -impl NextStage for Stage3 { - type BaseTable = u64; // ? - type Resolution = u64; // ? - fn resolve(_table: &Self::BaseTable) -> Self::Resolution { - 0u64 - } -} - -impl NextStage for Stage4 { - type BaseTable = u64; // ? - type Resolution = u64; // ? - fn resolve(_table: &Self::BaseTable) -> Self::Resolution { - 0u64 - } -} -impl NextStage for Stage4 { - type BaseTable = u64; // ? - type Resolution = u64; // ? - fn resolve(_table: &Self::BaseTable) -> Self::Resolution { - 0u64 - } -} -impl NextStage for Stage4 { - type BaseTable = u64; // ? - type Resolution = u64; // ? - fn resolve(_table: &Self::BaseTable) -> Self::Resolution { - 0u64 - } -} - -// Stage 1 always points to more tables -// impl NextLevelDescriptor for Stage1 { -// fn get_next_level_descriptor(table: &AssociatedTranslationTable) -> impl Descriptor { -// // check validity bits -// // return next descriptor from the associated table -// } -// } - -// impl NextLevelDescriptor for Stage2 {} - -// impl NextLevelDescriptor for Stage3 {} - -// impl LeafDescriptor for Stage4 {} - -type TraverseResult = Result; -enum Output { - Table(TableDescriptor), // duh need to put next table type in here ... - Block(BlockDescriptor), -} - -type ulog2 = usize; - -struct BlockDescriptor { - base_addr: PhysAddr, - size: ulog2, // should give a mask to combine with block offset from virt_addr -} - -trait TableOnly { - type NextTable; - fn next_table(&self, virt: VirtAddr) -> Self::NextTable; -} - -trait BlockOnly { - type Block; - fn block(&self, virt: VirtAddr) -> Self::Block; -} - -trait TableOrBlock: TableOnly + BlockOnly {} - -impl TableOnly for Stage1 { - type NextTable = TraverseResult; - fn next_table(&self, virt: VirtAddr) -> Self::NextTable { - Err(TraverseError::NotPresent) - } -} - -impl TableOnly for Stage2 { - type NextTable = TraverseResult; - fn next_table(&self, virt: VirtAddr) -> Self::NextTable { - Err(TraverseError::NotPresent) - } -} - -impl BlockOnly for Stage2 { - type Block = TraverseResult; - fn block(&self, virt: VirtAddr) -> Self::Block { - let index = virt >> Self::SHIFT & (1 << Self::MASK_BITS) - 1; - Err(TraverseError::NotPresent) - } -} - -impl TableOrBlock for Stage2 {} - -// -// Probably implement it as Granules vs Stages? -// Some systems may support variable granule sizes, some may not. -// Each Stage/Granule combination will yield bitmasks and/or bitfield! references -// and/or types of further stages (shall return enums?) -// - -// Specific granules - -struct Granule4k; -struct Granule16k; -struct Granule64k; - -impl GranuleSize for Granule4k {} -impl GranuleSize for Granule16k {} -impl GranuleSize for Granule64k {} - -// Masks to extract entry index from virt address for different stages and granule sizes -impl Granule for Stage1 { - const MASK_BITS: usize = 9; - const SHIFT: usize = 39; -} -impl Granule for Stage1 { - const MASK_BITS: usize = 1; - const SHIFT: usize = 47; -} -impl Granule for Stage1 { - const MASK_BITS: usize = 5; - const SHIFT: usize = 42; -} - -//------------------------ -// Page: with 4kb granule -//------------------------ - -/// A standard 4KiB page. -#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)] -pub enum Size4KiB {} - -impl PageSize for Size4KiB { - const SIZE_AS_DEBUG_STR: &'static str = "4KiB"; - const SHIFT: usize = 12; -} - -impl NotGiantPageSize for Size4KiB {} - -//------------------------- -// Page: with 16kb granule -//------------------------- - -/// A standard 16KiB page. -#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)] -pub enum Size16KiB {} - -impl PageSize for Size16KiB { - const SIZE_AS_DEBUG_STR: &'static str = "16KiB"; - const SHIFT: usize = 14; -} - -impl NotGiantPageSize for Size16KiB {} - -//------------------------- -// Page: with 64kb granule -//------------------------- - -/// A standard 64KiB page. -#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)] -pub enum Size64KiB {} - -impl PageSize for Size64KiB { - const SIZE_AS_DEBUG_STR: &'static str = "64KiB"; - const SHIFT: usize = 16; -} - -impl NotGiantPageSize for Size64KiB {} - -//-------------------------- -// Blocks: with 4kb granule -//-------------------------- - -/// A “huge” 2MiB page. -#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)] -pub enum Size2MiB {} - -impl PageSize for Size2MiB { - const SIZE_AS_DEBUG_STR: &'static str = "2MiB"; - const SHIFT: usize = 21; -} - -impl NotGiantPageSize for Size2MiB {} - -/// A “giant” 1GiB page. -#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)] -pub enum Size1GiB {} - -impl PageSize for Size1GiB { - const SIZE_AS_DEBUG_STR: &'static str = "1GiB"; - const SHIFT: usize = 30; -} - -//--------------------------- -// Blocks: with 16kb granule -//--------------------------- - -#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)] -pub enum Size32MiB {} - -impl PageSize for Size32MiB { - const SIZE_AS_DEBUG_STR: &'static str = "32MiB"; - const SHIFT: usize = 25; -} - -impl NotGiantPageSize for Size32MiB {} - -//--------------------------- -// Blocks: with 64kb granule -//--------------------------- - -#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)] -pub enum Size512MiB {} - -impl PageSize for Size512MiB { - const SIZE_AS_DEBUG_STR: &'static str = "512MiB"; - const SHIFT: usize = 29; -} - -impl NotGiantPageSize for Size512MiB {} - -/// The error returned by the `PageTableEntry::frame` method. -#[derive(Snafu, Debug, Clone, Copy, PartialEq)] -pub enum TraverseError { - /// The entry does not have the `PRESENT` flag set, so it isn't currently mapped to a frame. - NotPresent, -} - -// maestro: -// Calls `alloc` with order `order`. -// -// The allocated frame is in the kernel zone. -// -// The function returns the *virtual* address, not the physical one. -// pub fn alloc_kernel(order: FrameOrder) -> AllocResult> { -// let ptr = alloc(order, FLAG_ZONE_TYPE_KERNEL)?; -// let virt_ptr = memory::kern_to_virt(ptr.as_ptr()) as _; -// debug_assert!(virt_ptr as *const _ >= memory::PROCESS_END); -// NonNull::new(virt_ptr).ok_or(AllocError) -// } - -// Allocates a paging object and returns its virtual address. -// -// If the allocation fails, the function returns an error. -// fn alloc_obj() -> AllocResult<*mut u32> { -// let mut ptr = buddy::alloc_kernel(0)?.cast::(); -// // Zero memory -// let slice = unsafe { slice::from_raw_parts_mut(ptr.as_mut(), buddy::get_frame_size(0)) }; -// slice.fill(0); -// Ok(ptr.as_ptr() as _) -// } diff --git a/play/memtab2.rs b/play/memtab2.rs deleted file mode 100644 index ced54759b..000000000 --- a/play/memtab2.rs +++ /dev/null @@ -1,520 +0,0 @@ -//# snafu = "*" -//# either = "*" -// -// Explore memory table abstractions -// -// #![feature(decl_macro)] -#![feature(allocator_api)] -#![allow(unused)] -#![allow(unused_imports)] -use { - core::{ - marker::PhantomData, - ops::{Index, IndexMut}, - }, - either::*, - snafu::{ResultExt, Snafu}, - std::alloc::{alloc_zeroed, dealloc, Layout}, -}; - -type VirtAddr = u64; -type PhysAddr = u64; - -/// Provides means to extract the next table level index or the block address. -trait TableIndex { - // Mask and shift to extract entry index from virt address - const MASK_BITS: usize; - const MASK: u64 = 1 << Self::MASK_BITS - 1; - const SHIFT: usize; - const BLOCK_ADDR_MASK: u64; - const TABLE_ADDR_MASK: u64; - fn extract_index(address: VirtAddr) -> usize { - return ((address >> Self::SHIFT) & Self::MASK) - .try_into() - .expect("Arithmetics gone mad"); - } - // Extract address of a block with appropriate size (used by BlockOnly) - fn extract_block_base(entry: u64) -> u64 { - entry & Self::BLOCK_ADDR_MASK - } - // Extract address of the next table (use by TableOnly) -- aligned to the granule size for VMSAv8 - fn extract_table_base(entry: u64) -> u64 { - entry & Self::TABLE_ADDR_MASK - } -} - -// pub enum TraverseError { - -#[derive(Debug, Clone, Copy, PartialEq)] // Snafu -enum TableError { - /// The entry does not have the `PRESENT` flag set, so it isn't currently mapped to a frame. - NotPresent, -} - -trait TableOnly { - type NextTable; - fn next_table(&self, virt: VirtAddr) -> Result<&mut Self::NextTable, TableError>; -} - -#[derive(Debug, Clone, Copy, PartialEq)] // Snafu -enum BlockError { - /// The entry does not have the `PRESENT` flag set, so it isn't currently mapped to a frame. - NotPresent, -} - -trait BlockOnly { - type Block; - fn block(&self, virt: VirtAddr) -> Result; -} - -trait TableOrBlock { - type NextTable; - fn next_table(&self, virt: VirtAddr) -> Result<&mut Self::NextTable, TableError>; - type Block; - fn block(&self, virt: VirtAddr) -> Result; -} - -// @todo next() should also take a VirtAddr? -// trait NextStage: TableOrBlock { -// fn next( -// &self, -// ) -> Either, dyn BlockOnly>; -// } - -macro_rules! make_stage { - { name:$ident } => { - #[allow(non_camel_case_types)] - struct $name { - entries: [u64; 1 << Self::MASK_BITS], // u64 must be more flexible type EntryT for other arch's - } - } -} - -// As you can see, the Stage structures are repeated, as are the granules (masks), -// so we need to be able to parameterize them both somehow -// e.g. mask depends on both stage and granule -// These stage tables (or PageTable really) can be implemented in terms of TableIndex::MASK_BITS -// e.g. struct Stage { pub entries: [u64; 1 << I::MASK_BITS]; } -// Specific table structures: - -make_stage!(Stage1_Gran4k); -make_stage!(Stage2_Gran4k); -make_stage!(Stage3_Gran4k); -make_stage!(Stage4_Gran4k); - -make_stage!(Stage1_Gran16k); -make_stage!(Stage2_Gran16k); -make_stage!(Stage3_Gran16k); -make_stage!(Stage4_Gran16k); - -make_stage!(Stage1_Gran64k); -make_stage!(Stage2_Gran64k); -make_stage!(Stage3_Gran64k); - -macro_rules! impl_table_index { - { $stage:ty, index = $mask_bits:literal @ $shift:literal, table = $table_bits:literal @ $table_shift:literal, block = $block_bits:literal @ $block_shift:literal } => { - impl TableIndex for $stage { - const MASK_BITS: usize = $mask_bits; - const SHIFT: usize = $shift; - const BLOCK_ADDR_MASK: u64 = ((1 << $block_bits) - 1) << $block_shift; - const TABLE_ADDR_MASK: u64 = ((1 << $table_bits) - 1) << $table_shift; - } - } -} - -impl_table_index!(Stage1_Gran4k, index = 9@39, table = 36@12, block = 0@0); -impl_table_index!(Stage2_Gran4k, index = 9@30, table = 36@12, block = 18@30); -impl_table_index!(Stage3_Gran4k, index = 9@21, table = 36@12, block = 27@21); -impl_table_index!(Stage4_Gran4k, index = 9@12, table = 36@12, block = 36@12); - -impl_table_index!(Stage1_Gran16k, index = 1@47, table = 34@14, block = 0@0); -impl_table_index!(Stage2_Gran16k, index = 11@36, table = 34@14, block = 12@36); -impl_table_index!(Stage3_Gran16k, index = 11@25, table = 34@14, block = 23@25); -impl_table_index!(Stage4_Gran16k, index = 11@14, table = 34@14, block = 34@14); - -impl_table_index!(Stage1_Gran64k, index = 5@42, table = 32@16, block = 5@42); // 4TiB block! -impl_table_index!(Stage2_Gran64k, index = 13@29, table = 32@16, block = 19@29); -impl_table_index!(Stage3_Gran64k, index = 13@16, table = 32@16, block = 32@16); -// impl_table_index!(Stage4_Gran64k, index = 0@0); // N/A - -unsafe fn next_level(virt: VirtAddr) -> Result { - let index = <$stage>::extract_index(virt); - let entry = self.entries[index]; - if !bit_set(entry, P) { - return Err(TableError::NotPresent); - } - Ok(base) -} - -macro_rules! impl_table_only { - { $stage:ty, $next_stage:ty } => { - impl TableOnly for $stage where $stage: TableIndex { - type NextTable = $next_stage; - - fn next_table(&self, virt: VirtAddr) -> Result<&mut Self::NextTable, TableError> { - let entry = next_level(virt); - let base = <$stage>::extract_table_base(entry); // This involves some stage-dependent shenanigans, e.g. for RISC-V the PN[x] entries must be combined depending on the stage. - let next_table = base as *mut Self::NextTable; // ptr.cast<>? - let next_table = unsafe { &mut *next_table }; - Ok(next_table) - } - } - } -} - -// temp: -const P: usize = 0x0; - -fn bit_set(field: u64, bit: usize) -> bool { - field & (1 << bit) != 0 -} - -macro_rules! impl_block_only { - { $stage:ty, $block_size:ty } => { - impl BlockOnly for $stage where $stage: TableIndex { - type Block = Frame<$block_size>; // we will get a block of given size from this stage - - fn block(&self, virt: VirtAddr) -> Result { - let entry = next_level(virt); - let phys_base = <$stage>::extract_block_base(entry); - let block = Frame::new(phys_base.try_into().expect("It fits fine!")); - Ok(block) - } - } - } -} - -// Granularity 4KiB -impl_table_only!(Stage1_Gran4k, Stage2_Gran4k); - -impl_table_only!(Stage2_Gran4k, Stage3_Gran4k); -impl_block_only!(Stage2_Gran4k, Size1GiB); - -impl_table_only!(Stage3_Gran4k, Stage4_Gran4k); -impl_block_only!(Stage3_Gran4k, Size2MiB); - -impl_block_only!(Stage4_Gran4k, Size4KiB); - -// Granularity 16KiB -impl_table_only!(Stage1_Gran16k, Stage2_Gran16k); - -impl_table_only!(Stage2_Gran16k, Stage3_Gran16k); -impl_block_only!(Stage2_Gran16k, Size1GiB); - -impl_table_only!(Stage3_Gran16k, Stage4_Gran16k); -impl_block_only!(Stage3_Gran16k, Size2MiB); - -impl_block_only!(Stage4_Gran16k, Size4KiB); - -// Granularity 64KiB -impl_table_only!(Stage1_Gran64k, Stage2_Gran64k); -impl_block_only!(Stage1_Gran64k, Size4TiB); - -impl_table_only!(Stage2_Gran64k, Stage3_Gran64k); -impl_block_only!(Stage2_Gran64k, Size512MiB); - -impl_block_only!(Stage3_Gran64k, Size64KiB); - -macro_rules! impl_next_stage { - { $stage:ty, $next_stage:ty, $block:ty } => { - // @todo: Index for slicerange (restricted range! must maintain alignments) - // or range to fill in sequential table blocks on aarch64 - // e.g. stage4[0..15] = compound_large_segment; <- not using the VirtAddr here.. - impl Index for $stage - where - $stage: TableOnly, - { - type Output = Self::NextTable; - - fn index(&self, virt: VirtAddr) -> &Self::Output { - let tbl = <$stage as TableOnly>::next_table(virt); - &*tbl - } - } - - impl IndexMut for &mut $stage - where - $stage: TableOnly, - { - fn index_mut(&mut self, virt: VirtAddr) -> &mut Self::Output { - let index = Self::extract_index(virt); - Self::extract_table_base(self.entries[index]) - } - } - - impl Index for &$stage - where - $stage: BlockOnly, - { - type Output = ::Block; - - fn index(&self, virt: VirtAddr) -> &Self::Output { - let index = Self::extract_index(virt); - Self::extract_block_base(self.entries[index]) - } - } - - impl IndexMut for &mut $stage - where - $stage: BlockOnly, - { - // Should only return a mutable reference to entries[index] so that we - // could replace it. - fn index_mut(&mut self, virt: VirtAddr) -> &mut Self::Output { - let index = Self::extract_index(virt); - &mut self.entries[index] // but can't be just &mut u64, we need a proper table type? probably need to overload assignments too? - } - } - - // impl Index for T - // where - // T: TableIndex + TableOrBlock, - // { - // type Output = Either; - // fn index(&self, index: VirtAddr) -> &Self::Output { - // self.entries[index] - // } - // } - } -} - -// now for the TableOnly+BlockOnly combination we need to also provide a resolution method that returns either -impl_next_stage!(Stage1_Gran4k, Stage2_Gran4k, Size1GiB); // this provides next() -> Either -impl_next_stage!(Stage2_Gran4k, Stage3_Gran4k, Size2MiB); - -// @todo need to also provide next() for TableOnly and for BlockOnly types separately, without Either? - -// Level could be determining which of TableOnly, BlockOnly are implemented? (or implement Table/Block only FOR the Level?) -// Mask (TableIndex) gives extraction parameters for next table or next block. - -// Granule is not essential - it's arch specific and translates into number of Stages and -// TableIndex for ptr extraction. - -/// Abstract over the possible page sizes, 4KiB, 16KiB, 2MiB, 1GiB. -pub trait PageSize: Copy + PartialEq + Eq + PartialOrd + Ord { - /// A string representation of the page size for debug output. - const SIZE_AS_DEBUG_STR: &'static str; - - /// The page shift in bits. - const SHIFT: usize; - - /// The page size in bytes. - const SIZE: usize = 1 << Self::SHIFT; - - /// The page size mask in bits. - const MASK: u64 = 1 << Self::SHIFT - 1; - - fn alignment() -> usize { - Self::SIZE - } - - fn mask() -> u64 { - Self::MASK - } -} - -macro_rules! make_page { - { $name:ident, $debug_str:literal, $shift:literal } => { - #[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)] - pub enum $name {} - - impl PageSize for $name { - const SIZE_AS_DEBUG_STR: &'static str = $debug_str; - const SHIFT: usize = $shift; - } - } -} - -//------------------------ -// Page: with 4kb granule -//------------------------ - -// A standard 4KiB page. -make_page!(Size4KiB, "4KiB", 12); - -//------------------------- -// Page: with 16kb granule -//------------------------- - -// A standard 16KiB page. -make_page!(Size16KiB, "16KiB", 14); - -//------------------------- -// Page: with 64kb granule -//------------------------- - -// A standard 64KiB page. -make_page!(Size64KiB, "64KiB", 16); - -//-------------------------- -// Blocks: with 4kb granule -//-------------------------- - -// A “huge” 2MiB page. -make_page!(Size2MiB, "2MiB", 21); - -// A “giant” 1GiB page. -make_page!(Size1GiB, "1GiB", 30); - -//--------------------------- -// Blocks: with 16kb granule -//--------------------------- - -make_page!(Size32MiB, "32MiB", 25); - -//--------------------------- -// Blocks: with 64kb granule -//--------------------------- - -make_page!(Size512MiB, "512MiB", 29); - -make_page!(Size4TiB, "4TiB", 42); - -/// Physical page frame. -struct Frame { - base: usize, - _page_size: PhantomData

, -} - -impl Frame

{ - // @todo Check base is aligned to _page_size::SHIFT - pub fn new(base: usize) -> Self { - Self { - base, - _page_size: PhantomData, - } - } -} - -struct PageTableAllocator; -struct FrameAllocator; - -#[derive(Debug, Snafu)] -enum AllocError { - InvalidLayout { source: std::alloc::LayoutError }, -} - -impl PageTableAllocator { - fn alloc() -> Result<&mut T, AllocError> { - let size = (1 << T::MASK_BITS) * core::mem::size_of::(); - let layout = Layout::from_size_align(size, size).context(InvalidLayoutSnafu)?; - println!( - "Allocating page table: size {}, align {}", - layout.size(), - layout.align() - ); - let ptr = unsafe { alloc_zeroed(layout) }; - assert_ne!(ptr as usize, 0); - Ok(ptr as *mut T) - } - fn dealloc(ptr: *mut T) -> Result<(), AllocError> { - let size = (1 << T::MASK_BITS) * core::mem::size_of::(); - let layout = Layout::from_size_align(size, size).context(InvalidLayoutSnafu)?; - unsafe { dealloc(ptr as *mut u8, layout) }; - Ok(()) - } -} - -impl FrameAllocator { - fn alloc() -> Result, AllocError> { - let layout = - Layout::from_size_align(1 << P::SHIFT, 1 << P::SHIFT).context(InvalidLayoutSnafu)?; - println!( - "Allocating frame: size {}, align {}", - layout.size(), - layout.align() - ); - let ptr = unsafe { alloc_zeroed(layout) }; - assert_ne!(ptr as usize, 0); - Ok(Frame::new(ptr as usize)) - } - fn dealloc(ptr: Frame

) -> Result<(), AllocError> { - let layout = - Layout::from_size_align(1 << P::SHIFT, 1 << P::SHIFT).context(InvalidLayoutSnafu)?; - unsafe { dealloc(ptr.base as *mut u8, layout) }; - Ok(()) - } -} - -fn main() -> Result<(), AllocError> { - println!("Hello, play"); - - println!("Stage1_Gran4k {}", core::mem::size_of::()); - println!("Stage2_Gran4k {}", core::mem::size_of::()); - println!("Stage3_Gran4k {}", core::mem::size_of::()); - println!("Stage4_Gran4k {}", core::mem::size_of::()); - - println!("Stage1_Gran16k {}", core::mem::size_of::()); - println!("Stage2_Gran16k {}", core::mem::size_of::()); - println!("Stage3_Gran16k {}", core::mem::size_of::()); - println!("Stage4_Gran16k {}", core::mem::size_of::()); - - println!("Stage1_Gran64k {}", core::mem::size_of::()); - println!("Stage2_Gran64k {}", core::mem::size_of::()); - println!("Stage3_Gran64k {}", core::mem::size_of::()); - println!("Stage4_Gran64k {}", core::mem::size_of::()); - - // Build page table hierarchy from Stage1 down to Stage4. - let virt: VirtAddr = 0xdead_beef; - - // Using Granule4k: - let s1_table = PageTableAllocator::alloc::()?; - let s2_table = PageTableAllocator::alloc::()?; - s1_table[virt] = s2_table; - let s3_table = PageTableAllocator::alloc::()?; - s2_table[virt] = s3_table; - let s4_table = PageTableAllocator::alloc::()?; - s3_table[virt] = s4_table; - let leaf = FrameAllocator::alloc::()?; - s4_table[virt] = leaf; - - // Using Granule16k: - let mut s1_table = PageTableAllocator::alloc::()?; - let mut s2_table = PageTableAllocator::alloc::()?; - s1_table[virt] = s2_table; - let mut s3_table = PageTableAllocator::alloc::()?; - s2_table[virt] = s3_table; - let mut s4_table = PageTableAllocator::alloc::()?; - s3_table[virt] = s4_table; - let leaf = FrameAllocator::alloc::()?; - s4_table[virt] = leaf; - - // This should fail to compile (accidentally wrong stage granule) - let mut s3_table = PageTableAllocator::alloc::()?; - s2_table[virt] = s3_table; - - // This too (accidentally wrong table level) - let s3_table = PageTableAllocator::alloc::()?; - s2_table[virt] = s3_table; - - // // Using Granule64k: - let s1_table = PageTableAllocator::alloc::()?; - let s2_table = PageTableAllocator::alloc::()?; - s1_table[virt] = s2_table; - let s3_table = PageTableAllocator::alloc::()?; - s2_table[virt] = s3_table; - let leaf = FrameAllocator::alloc::()?; - s3_table[virt] = leaf; - - //================= - // TRAVERSE TABLES - //================= - - let virt: VirtAddr = 0xdead_beef; - - // let tt_base_addr = 124467000usize; // this comes from TTBRx register or some table base variable for each process. - - // // everything starts with a translation table base address - // let sys_l0_table = base_addr as *const Stage1; - - // // Access the table using virtual addresses (each stage consumes more bits of the address). - // let l0_table = sys_l0_table; - // let l1 = l0_table[virt_addr]; - // let l2 = l1_table[virt_addr]; - // let phys = l2_table[virt_addr]; - - // // to access physical memory from kernel - // let phys = 123456usize; - // let kern_phys = phys.phys_to_kernel(); - Ok(()) -} From 9b106c2322009e7fec997964963ce994903f0ac5 Mon Sep 17 00:00:00 2001 From: Berkus Decker Date: Mon, 9 Feb 2026 10:05:52 +0200 Subject: [PATCH 072/107] wip: Add x86_64 --- libs/memory/src/arch/mod.rs | 2 + libs/memory/src/arch/x86_64.rs | 258 +++++++++++++++++++++++++++++++++ libs/memory/src/lib.rs | 2 + 3 files changed, 262 insertions(+) create mode 100644 libs/memory/src/arch/x86_64.rs diff --git a/libs/memory/src/arch/mod.rs b/libs/memory/src/arch/mod.rs index 06905431f..267652ebb 100644 --- a/libs/memory/src/arch/mod.rs +++ b/libs/memory/src/arch/mod.rs @@ -1,2 +1,4 @@ #[cfg(target_arch = "aarch64")] pub mod aarch64; + +pub mod x86_64; diff --git a/libs/memory/src/arch/x86_64.rs b/libs/memory/src/arch/x86_64.rs new file mode 100644 index 000000000..99abf76f1 --- /dev/null +++ b/libs/memory/src/arch/x86_64.rs @@ -0,0 +1,258 @@ +use { + crate::arch_trait::{EntryKind, LevelCapabilities, TranslationArch}, + libaddress::{PhysAddr, VirtAddr}, + libmapping::{AccessPermissions, AttributeFields, MemAttributes}, +}; + +// --------------------------------------------------------------------------- +// x86_64 page table entry bit layout (4-level, 4KiB pages) +// --------------------------------------------------------------------------- +// +// Virtual address split (48-bit canonical): +// +// 63-48 47-39 38-30 29-21 20-12 11-0 +// signx PML4 PDPT PD PT off +// (copy L0 L1 L2 L3 +// of b47) +// +// Each level indexes 512 entries (9 bits). Table size = 512 * 8 = 4096 bytes. +// +// L0 (PML4): table pointer only +// L1 (PDPT): table pointer or 1GiB page (if PS bit set) +// L2 (PD): table pointer or 2MiB page (if PS bit set) +// L3 (PT): 4KiB page only +// +// Common entry format: +// +// [63] NX (No Execute) +// [62:52] available / reserved (depending on feature) +// [51:12] physical address (4K aligned; [51:M] must be 0 where M = MAXPHYADDR) +// [11:9] available +// [8] G (Global, ignored in PML4E/PDPTE) +// [7] PS (Page Size: 0=points to table, 1=maps large page; must be 0 in PML4E and PTE) +// [6] D (Dirty, only on leaf entries) +// [5] A (Accessed) +// [4] PCD (Page-level Cache Disable) +// [3] PWT (Page-level Write-Through) +// [2] U/S (User/Supervisor: 0=supervisor only, 1=user accessible) +// [1] R/W (Read/Write: 0=read-only, 1=writable) +// [0] P (Present) +// +// Large page (1GiB at L1, 2MiB at L2): +// Same as above but PS=1, and address bits below the page size are +// repurposed: [20:13] = PAT/reserved for 2MiB, [29:13] for 1GiB. + +// Entry bit positions +const PRESENT: u64 = 1 << 0; +const WRITABLE: u64 = 1 << 1; +const USER_ACCESSIBLE: u64 = 1 << 2; +const WRITE_THROUGH: u64 = 1 << 3; +const CACHE_DISABLE: u64 = 1 << 4; +const ACCESSED: u64 = 1 << 5; +const DIRTY: u64 = 1 << 6; +const PAGE_SIZE: u64 = 1 << 7; // PS bit — large page at L1/L2 +const NO_EXECUTE: u64 = 1 << 63; + +// Address masks +const ADDR_MASK_4K: u64 = 0x000F_FFFF_FFFF_F000; // [51:12] +const ADDR_MASK_2M: u64 = 0x000F_FFFF_FFE0_0000; // [51:21] +const ADDR_MASK_1G: u64 = 0x000F_FFFF_C000_0000; // [51:30] + +// Block sizes +const SIZE_4K: usize = 4096; +const SIZE_2M: usize = 2 * 1024 * 1024; +const SIZE_1G: usize = 1024 * 1024 * 1024; + +/// x86_64 4-level paging with 4KiB pages. +/// +/// 4-level hierarchy: PML4 (L0) -> PDPT (L1) -> PD (L2) -> PT (L3). +/// 512 entries per table, 4096-byte table size, 4096-byte alignment. +/// +/// Supports 1GiB huge pages at L1 (PDPT) and 2MiB large pages at L2 (PD) +/// via the PS (Page Size) bit. +pub struct X86_64_4K; + +impl TranslationArch for X86_64_4K { + const NUM_LEVELS: usize = 4; + + fn entries_per_table(_level: usize) -> usize { + 512 + } + + fn table_alignment(_level: usize) -> usize { + SIZE_4K + } + + fn level_capabilities(level: usize) -> LevelCapabilities { + match level { + // PML4: table pointer only (no large pages) + 0 => LevelCapabilities { + supports_table_pointer: true, + supports_block: false, + block_size: 0, + }, + // PDPT: table pointer or 1GiB huge page + 1 => LevelCapabilities { + supports_table_pointer: true, + supports_block: true, + block_size: SIZE_1G, + }, + // PD: table pointer or 2MiB large page + 2 => LevelCapabilities { + supports_table_pointer: true, + supports_block: true, + block_size: SIZE_2M, + }, + // PT: 4KiB page only + 3 => LevelCapabilities { + supports_table_pointer: false, + supports_block: true, + block_size: SIZE_4K, + }, + _ => LevelCapabilities { + supports_table_pointer: false, + supports_block: false, + block_size: 0, + }, + } + } + + fn index_from_vaddr(vaddr: VirtAddr, level: usize) -> usize { + let shift = match level { + 0 => 39, // PML4 index + 1 => 30, // PDPT index + 2 => 21, // PD index + 3 => 12, // PT index + _ => return 0, + }; + ((vaddr.as_u64() >> shift) & 0x1FF) as usize + } + + fn decode_entry(raw: u64, level: usize) -> EntryKind { + if raw & PRESENT == 0 { + return EntryKind::Invalid; + } + + match level { + // PML4: always a table pointer (PS must be 0) + 0 => EntryKind::Table(PhysAddr::new(raw & ADDR_MASK_4K)), + // PDPT: PS=1 means 1GiB page, PS=0 means table pointer + 1 => { + if raw & PAGE_SIZE != 0 { + EntryKind::Block(PhysAddr::new(raw & ADDR_MASK_1G)) + } else { + EntryKind::Table(PhysAddr::new(raw & ADDR_MASK_4K)) + } + } + // PD: PS=1 means 2MiB page, PS=0 means table pointer + 2 => { + if raw & PAGE_SIZE != 0 { + EntryKind::Block(PhysAddr::new(raw & ADDR_MASK_2M)) + } else { + EntryKind::Table(PhysAddr::new(raw & ADDR_MASK_4K)) + } + } + // PT: always a 4KiB page (PS is ignored / must be 0) + 3 => EntryKind::Block(PhysAddr::new(raw & ADDR_MASK_4K)), + _ => EntryKind::Invalid, + } + } + + fn encode_table_entry(next_table_phys: PhysAddr, _level: usize) -> u64 { + let addr = next_table_phys.as_u64(); + debug_assert!(addr & !ADDR_MASK_4K == 0, "Table address not 4K aligned"); + // Table entries: P=1, R/W=1, U/S=1 (permissive — leaf entries restrict further) + (addr & ADDR_MASK_4K) | USER_ACCESSIBLE | WRITABLE | PRESENT + } + + fn encode_block_entry(phys: PhysAddr, attr: AttributeFields, level: usize) -> u64 { + let addr = phys.as_u64(); + let addr_mask = match level { + 1 => ADDR_MASK_1G, + 2 => ADDR_MASK_2M, + _ => panic!("Large page entries only valid at L1 (1GiB) and L2 (2MiB)"), + }; + debug_assert!( + addr & !addr_mask == 0, + "Block address not aligned for level" + ); + // PS=1 for large pages at L1/L2 + (addr & addr_mask) | PAGE_SIZE | encode_attributes(attr) | PRESENT + } + + fn encode_page_entry(phys: PhysAddr, attr: AttributeFields) -> u64 { + let addr = phys.as_u64(); + debug_assert!(addr & !ADDR_MASK_4K == 0, "Page address not 4K aligned"); + // PT entries: no PS bit, just P=1 + (addr & ADDR_MASK_4K) | encode_attributes(attr) | PRESENT + } + + fn output_address(raw: u64, level: usize) -> PhysAddr { + let mask = match level { + 0 => ADDR_MASK_4K, + 1 => { + if raw & PAGE_SIZE != 0 { + ADDR_MASK_1G + } else { + ADDR_MASK_4K + } + } + 2 => { + if raw & PAGE_SIZE != 0 { + ADDR_MASK_2M + } else { + ADDR_MASK_4K + } + } + 3 => ADDR_MASK_4K, + _ => 0, + }; + PhysAddr::new(raw & mask) + } +} + +/// Encode `AttributeFields` into the x86_64 PTE flag bits. +/// +/// x86_64 uses a different model from ARM: +/// - No MAIR — caching is controlled by PWT and PCD bits directly +/// - Permissions are R/W (bit 1) and U/S (bit 2) +/// - Execute control is NX (bit 63), opt-out rather than opt-in +fn encode_attributes(attr: AttributeFields) -> u64 { + let mut bits: u64 = 0; + + // Accessed and Dirty — pre-set to avoid faults on first access. + // The kernel can clear these for tracking if needed. + bits |= ACCESSED | DIRTY; + + // Memory type via PWT/PCD + match attr.mem_attributes { + MemAttributes::CacheableDRAM => { + // PWT=0, PCD=0: write-back cacheable + } + MemAttributes::NonCacheableDRAM => { + // PWT=1, PCD=1: uncacheable (UC) + // For true WC you'd use PAT, but UC is the safe no_std default. + bits |= WRITE_THROUGH | CACHE_DISABLE; + } + MemAttributes::Device => { + // PWT=0, PCD=1: uncacheable, strong ordering + bits |= CACHE_DISABLE; + } + } + + // Access permissions — x86_64 is permissive by default (read always allowed). + // R/W bit: 0 = read-only, 1 = read-write + if let AccessPermissions::ReadWrite = attr.acc_perms { + bits |= WRITABLE; + } + + // Execute-never via NX bit + if attr.execute_never { + bits |= NO_EXECUTE; + } + + // Supervisor-only for now (U/S = 0) + // When userspace is implemented, set USER_ACCESSIBLE based on context. + + bits +} diff --git a/libs/memory/src/lib.rs b/libs/memory/src/lib.rs index 7f83b5877..c45d65c45 100644 --- a/libs/memory/src/lib.rs +++ b/libs/memory/src/lib.rs @@ -35,3 +35,5 @@ pub use { // Re-export arch implementations. #[cfg(target_arch = "aarch64")] pub use arch::aarch64::{Aarch64_4K, features, mmu}; + +pub use arch::x86_64::X86_64_4K; From 22247768e657db78e98690fbada4b1c1beb649d5 Mon Sep 17 00:00:00 2001 From: Berkus Decker Date: Mon, 9 Feb 2026 10:52:57 +0200 Subject: [PATCH 073/107] wip: Add RISC-V64 --- libs/memory/src/arch/mod.rs | 1 + libs/memory/src/arch/riscv64.rs | 264 ++++++++++++++++++++++++++++++++ libs/memory/src/lib.rs | 2 +- 3 files changed, 266 insertions(+), 1 deletion(-) create mode 100644 libs/memory/src/arch/riscv64.rs diff --git a/libs/memory/src/arch/mod.rs b/libs/memory/src/arch/mod.rs index 267652ebb..f91f7a03d 100644 --- a/libs/memory/src/arch/mod.rs +++ b/libs/memory/src/arch/mod.rs @@ -1,4 +1,5 @@ #[cfg(target_arch = "aarch64")] pub mod aarch64; +pub mod riscv64; pub mod x86_64; diff --git a/libs/memory/src/arch/riscv64.rs b/libs/memory/src/arch/riscv64.rs new file mode 100644 index 000000000..6c32c641e --- /dev/null +++ b/libs/memory/src/arch/riscv64.rs @@ -0,0 +1,264 @@ +use { + crate::arch_trait::{EntryKind, LevelCapabilities, TranslationArch}, + libaddress::{PhysAddr, VirtAddr}, + libmapping::{AccessPermissions, AttributeFields, MemAttributes}, +}; + +// --------------------------------------------------------------------------- +// RISC-V Sv48 page table entry bit layout (4-level, 4KiB pages) +// --------------------------------------------------------------------------- +// +// Sv48 virtual address split (48-bit, sign-extended from bit 47): +// +// 63-48 47-39 38-30 29-21 20-12 11-0 +// signx VPN[3] VPN[2] VPN[1] VPN[0] off +// L0 L1 L2 L3 +// +// Each level indexes 512 entries (9 bits). Table size = 512 * 8 = 4096 bytes. +// +// Any non-leaf level can produce a superpage if R, W, or X bits are set: +// L0 (VPN[3]): 512 GiB terapage (rare, usually table-only) +// L1 (VPN[2]): 1 GiB gigapage +// L2 (VPN[1]): 2 MiB megapage +// L3 (VPN[0]): 4 KiB page (always leaf) +// +// PTE format (64 bits): +// +// [63:54] reserved (must be 0) +// [53:10] PPN (Physical Page Number) — PPN[2] [53:28], PPN[1] [27:19], PPN[0] [18:10] +// [9:8] RSW (reserved for supervisor software) +// [7] D (Dirty) +// [6] A (Accessed) +// [5] G (Global) +// [4] U (User accessible) +// [3] X (Executable) +// [2] W (Writable) +// [1] R (Readable) +// [0] V (Valid) +// +// Leaf vs non-leaf determination: +// V=1 and R=0, W=0, X=0 => pointer to next-level table +// V=1 and (R=1 or X=1) => leaf page/superpage +// V=0 => invalid +// W=1 and R=0 => reserved (invalid) + +// PTE flag bits +const V: u64 = 1 << 0; // Valid +const R: u64 = 1 << 1; // Readable +const W: u64 = 1 << 2; // Writable +const X: u64 = 1 << 3; // Executable +const _U: u64 = 1 << 4; // User accessible (reserved for userspace support) +const _G: u64 = 1 << 5; // Global (reserved for global mappings) +const A: u64 = 1 << 6; // Accessed +const D: u64 = 1 << 7; // Dirty + +// PPN extraction: PTE bits [53:10] contain the physical page number. +// Physical address = PPN << 12. +const PPN_SHIFT: u64 = 10; +const PPN_MASK: u64 = 0x003F_FFFF_FFFF_FC00; // bits [53:10] + +// For superpages, lower PPN fields must be zero: +// 2MiB megapage (L2): PPN[0] (bits [18:10]) must be 0 +// 1GiB gigapage (L1): PPN[0] and PPN[1] (bits [27:10]) must be 0 +// 512GiB terapage (L0): PPN[0], PPN[1], PPN[2] partially (bits [37:10]) must be 0 + +// Block sizes +const SIZE_4K: usize = 4096; +const SIZE_2M: usize = 2 * 1024 * 1024; +const SIZE_1G: usize = 1024 * 1024 * 1024; +const SIZE_512G: usize = 512 * 1024 * 1024 * 1024; + +/// Convert a PPN from a PTE into a physical address. +fn ppn_to_phys(raw: u64) -> u64 { + ((raw & PPN_MASK) >> PPN_SHIFT) << 12 +} + +/// Convert a physical address into PPN bits positioned for a PTE. +fn phys_to_ppn(phys: u64) -> u64 { + (phys >> 12) << PPN_SHIFT +} + +/// Is this PTE a leaf (page/superpage)? +/// A valid entry is a leaf if any of R, W, X are set. +fn is_leaf(raw: u64) -> bool { + raw & (R | W | X) != 0 +} + +/// RISC-V Sv48 4-level paging. +/// +/// 4-level hierarchy with 512 entries per table, 4KiB alignment. +/// Supports superpages at L0 (512GiB), L1 (1GiB), L2 (2MiB), and +/// regular 4KiB pages at L3. +/// +/// Unlike AArch64 and x86_64, RISC-V uses the same PTE format at every +/// level. The distinction between table pointer and leaf is purely based +/// on the R/W/X permission bits: if none are set, it's a table pointer. +#[allow(non_camel_case_types)] +pub struct RiscV_Sv48; + +impl TranslationArch for RiscV_Sv48 { + const NUM_LEVELS: usize = 4; + + fn entries_per_table(_level: usize) -> usize { + 512 + } + + fn table_alignment(_level: usize) -> usize { + SIZE_4K + } + + fn level_capabilities(level: usize) -> LevelCapabilities { + match level { + // L0: table pointer or 512GiB terapage (generally table-only in practice) + 0 => LevelCapabilities { + supports_table_pointer: true, + supports_block: true, + block_size: SIZE_512G, + }, + // L1: table pointer or 1GiB gigapage + 1 => LevelCapabilities { + supports_table_pointer: true, + supports_block: true, + block_size: SIZE_1G, + }, + // L2: table pointer or 2MiB megapage + 2 => LevelCapabilities { + supports_table_pointer: true, + supports_block: true, + block_size: SIZE_2M, + }, + // L3: 4KiB page only (leaf only) + 3 => LevelCapabilities { + supports_table_pointer: false, + supports_block: true, + block_size: SIZE_4K, + }, + _ => LevelCapabilities { + supports_table_pointer: false, + supports_block: false, + block_size: 0, + }, + } + } + + fn index_from_vaddr(vaddr: VirtAddr, level: usize) -> usize { + let shift = match level { + 0 => 39, // VPN[3] + 1 => 30, // VPN[2] + 2 => 21, // VPN[1] + 3 => 12, // VPN[0] + _ => return 0, + }; + ((vaddr.as_u64() >> shift) & 0x1FF) as usize + } + + fn decode_entry(raw: u64, _level: usize) -> EntryKind { + if raw & V == 0 { + return EntryKind::Invalid; + } + + // W=1, R=0 is a reserved combination + if raw & W != 0 && raw & R == 0 { + return EntryKind::Invalid; + } + + if is_leaf(raw) { + EntryKind::Block(PhysAddr::new(ppn_to_phys(raw))) + } else { + EntryKind::Table(PhysAddr::new(ppn_to_phys(raw))) + } + } + + fn encode_table_entry(next_table_phys: PhysAddr, _level: usize) -> u64 { + let addr = next_table_phys.as_u64(); + debug_assert!(addr & 0xFFF == 0, "Table address not 4K aligned"); + // V=1, R=W=X=0 => non-leaf (table pointer) + phys_to_ppn(addr) | V + } + + fn encode_block_entry(phys: PhysAddr, attr: AttributeFields, level: usize) -> u64 { + let addr = phys.as_u64(); + + // Verify superpage alignment: lower PPN fields must be zero + match level { + 0 => debug_assert!( + addr & (SIZE_512G as u64 - 1) == 0, + "512GiB terapage not aligned" + ), + 1 => debug_assert!( + addr & (SIZE_1G as u64 - 1) == 0, + "1GiB gigapage not aligned" + ), + 2 => debug_assert!( + addr & (SIZE_2M as u64 - 1) == 0, + "2MiB megapage not aligned" + ), + _ => panic!("Use encode_page_entry for L3 leaf entries"), + } + + phys_to_ppn(addr) | encode_attributes(attr) | V + } + + fn encode_page_entry(phys: PhysAddr, attr: AttributeFields) -> u64 { + let addr = phys.as_u64(); + debug_assert!(addr & 0xFFF == 0, "Page address not 4K aligned"); + phys_to_ppn(addr) | encode_attributes(attr) | V + } + + fn output_address(raw: u64, _level: usize) -> PhysAddr { + PhysAddr::new(ppn_to_phys(raw)) + } +} + +/// Encode `AttributeFields` into RISC-V PTE permission/attribute bits. +/// +/// RISC-V is simpler than ARM/x86 — no MAIR or PWT/PCD. Memory type +/// is controlled by the Svpbmt extension (bits [62:61]) when available, +/// but the base spec treats all memory as cacheable. For device memory +/// we rely on Svpbmt or PMA (Physical Memory Attributes) configured +/// elsewhere. +fn encode_attributes(attr: AttributeFields) -> u64 { + let mut bits: u64 = 0; + + // Pre-set Accessed and Dirty to avoid page faults on first access. + bits |= A | D; + + // RISC-V permission model: R, W, X bits directly. + // At minimum, a leaf entry must have R or X set. + match attr.acc_perms { + AccessPermissions::ReadWrite => bits |= R | W, + AccessPermissions::ReadOnly => bits |= R, + } + + if !attr.execute_never { + bits |= X; + } + + // Supervisor-only for now (U=0). + // When userspace is implemented, set U based on context. + + // Memory type hints via Svpbmt (bits [62:61]) when the extension is + // available. Without Svpbmt, PMAs define memory type and these bits + // are reserved-zero. + // + // Svpbmt encoding: + // 00 = PMA (default, use Physical Memory Attributes) + // 01 = NC (non-cacheable) + // 10 = IO (I/O, strongly ordered) + // 11 = reserved + match attr.mem_attributes { + MemAttributes::CacheableDRAM => { + // 00 = PMA default (cacheable for normal RAM) + } + MemAttributes::NonCacheableDRAM => { + // 01 = NC + bits |= 1 << 61; + } + MemAttributes::Device => { + // 10 = IO + bits |= 2 << 61; + } + } + + bits +} diff --git a/libs/memory/src/lib.rs b/libs/memory/src/lib.rs index c45d65c45..d6ff159e0 100644 --- a/libs/memory/src/lib.rs +++ b/libs/memory/src/lib.rs @@ -36,4 +36,4 @@ pub use { #[cfg(target_arch = "aarch64")] pub use arch::aarch64::{Aarch64_4K, features, mmu}; -pub use arch::x86_64::X86_64_4K; +pub use arch::{riscv64::RiscV_Sv48, x86_64::X86_64_4K}; From 745b128aaf4f8246aada48e799bc93ec10e4b16e Mon Sep 17 00:00:00 2001 From: Berkus Decker Date: Mon, 9 Feb 2026 10:59:34 +0200 Subject: [PATCH 074/107] wip: Add PowerPC --- libs/memory/TODO.md | 2 + libs/memory/docs/970MP_um.2008MAR07_pub.pdf | Bin 0 -> 3901658 bytes libs/memory/src/arch/mod.rs | 1 + libs/memory/src/arch/powerpc.rs | 328 ++++++++++++++++++++ libs/memory/src/arch_trait.rs | 74 ++++- libs/memory/src/lib.rs | 4 +- libs/memory/src/table.rs | 105 +++++-- libs/memory/src/walk.rs | 89 +++++- 8 files changed, 569 insertions(+), 34 deletions(-) create mode 100644 libs/memory/TODO.md create mode 100644 libs/memory/docs/970MP_um.2008MAR07_pub.pdf create mode 100644 libs/memory/src/arch/powerpc.rs diff --git a/libs/memory/TODO.md b/libs/memory/TODO.md new file mode 100644 index 000000000..45ba1473c --- /dev/null +++ b/libs/memory/TODO.md @@ -0,0 +1,2 @@ +- [x] Expand to include PowerPC +- [ ] Expand to include other granule sizes (16K and 64K for arm, to test how it scales) diff --git a/libs/memory/docs/970MP_um.2008MAR07_pub.pdf b/libs/memory/docs/970MP_um.2008MAR07_pub.pdf new file mode 100644 index 0000000000000000000000000000000000000000..b6bc155d0d9a7ddd76a6392a5fc68e0a19e8be00 GIT binary patch literal 3901658 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%0D^2L6r7@G zfTC#ZSq;bylcAKY8Kj39d&ppKJ)>J-jT2g3fS_w0oz937T`Jf@aWwnzI8h1_sbXxl zzG#GCCzKlcD`chBQvFmS+2p7zpc9i&A(g;hte zSX5YiF#p2>{)rTBIsuqf%T$en@=etn%9v_m*OxC~O!Fy_x`E_?xtP8^k^>f1ktRbJi5PEfw~*z& zECMs486^+xLP2yV<5e$?6$IV2n1rE;ATyM?FM{;*

k)oW)oV?u;*qUg)sljt!3s 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 $6w!_&uV9J?%`T~5uR>)cbbDOVq9^Lsslu* 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

&9rfRCWk#{wJPD{EF?O9bBOIIexR91=XvNr7

P4B0Ch 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 z z%G?!iGdj?g?l@4z z<03g6vo*Ja>ESnn@D+QVgp`L6Bnm*6h4}S$1`rHm_@a*@&J)Z-5$e9Nfs5}nnp*s~ z4nEoSc7uDJ!*e=o*8}F>-;l1E@qS9*qR+B4+gMqPftX(=Dh(X3RV#n-;BVn_Ik4QS z!{DpA(JN1W*gj|N(fFtUc>7>eeE8hAuil1?zdst&ge_z_;4A8;f zn{XF;_2H9C0p_MwPQnr4nLR>)g1&{KPJ=US*=QvGfnUzxjY*b@PZOrJ#uG5%!sj$= zRVjd*eQ(zJeAn9K_b~59yx$QiQxfyrr&F0-K=OHZfYfabwj#?|l-;@UtjZi|+%H}m z<+(J3LfzP|>6Fa{G_kkwTANl0$`d7oO)TO%s@Hnw8cq96;o)JIdB7FrcH_w1OQqst zSu0j)G@}Q*574cnNkMy(UWEWY=d<_}=o}}-e%lw^XLsEV$MJ<rtOh)PT z4Y4{n{qlf;M7IP>(L4+3s+^6+ch!3JF?wLRyT7VW_tTnFUuf6ODP@+bJttT2fE#5p zUMwK_6bBTy*aY=}El=mu_ltba@#xS-`)zT{u-Zd*aI7|)+zsWe2Q?q-^Sl4vN{GhA zmtH+#B?Rehq?V21(cj39as)Ptx3&=HEf&XrMelM5H7tDY^C;lw__)J3UZy1Lz(TMj z>+=y*I(0>e7dt$U*ATcKMFk(0hkJyHtI(C$O&8>HsBU5v1XwjhOc9eolBoNm zsESF@Y5Dlex=Vyw?*pXI%x$D$l-&xug(8P9_c+^Aqhizi>T=ll9|b*{_9$$Do2=>- zBGi-sasZ)A19CMY!4THt5C?{5&Pbd{g)|hg$@m-kZ}j5!@|Rd@tEV<}i=TKrOc_;G zmNgD9llKr7OFdeZJ%j+XIPz!to<2s%9YY5qH_nhGUIe91*@t^rtMwXZE8rfJsaxSj zZ~kW+OWaUs6%$anH{rDnGK8A;&b32=oOfM!aLNviOb?_Fl$E(Eao^wQ$4w(us?K57 z4bRD)H=v;+(w%u0hf=*yd6t8De7FpbNOR86fXFFMt^sjhp&)Vim^~EwX!2BFh<#H% z%azR{K?}v@%(Cl*kX1nyV^tMlpw_rkd!XNU#=F3TM4)pe(Xj#%(4Fle>-lPWWyH>+ z;eU5EX90e#!{%9XgLq{qUR5{g-K2h(Wzw5RG89r}qsnqqC7 zT}uB{UB%NSw<$*NpAT`C?E;??_U+Z0byOIjdYAK3C8o4}N z{3Sm_WDcQ>LQM%j?DRdiGBvRfjb^%I&OHD{RHK7;(G*g)BC-lU zRA}_cYsP}vSe5Sr4kLt}0K%UFGNESEdf?zDM=qY!aIq;uZ)|q1@fSmq9%kpdeqbwc zfiR%hG|otMG7g8+EM+1HO71|=IQMhLFiTlFzt7p|m^ES%6TQ8Eh(Q0$(d zE9JH=Ze@f#k?$$dPfdAWa9gllfe}TXt5F^pAPWgviIi7+-o6e#@_YLB=~``b>Gp>p z&_x9cJ)s+^&f?A}eZdkG8l1juV6aEj_(z;mlo8sqX9If_4Hhiur>33{cn6rswJ>B- zciVw`R|}P7l}4_=TZic{3Dq%pT+3s_F`f7xYfGuuGUxrZ{!`t{(qFI3hK=VK6 zkv*Q4mH^>dr!wRhp}_&GUR%Wm#KYFHJ;bCu5E=kMuzQ@nbL7nuuF=etc#9zscG*)4 zcRCGqxI0U>UTW~sm8$x*+&KCf_oi>K9L|xa$LFRbLTSbLYG6#ND;hDJ-0>G(7~18f zQCk)C5xp7cU9wvz(Kl>68A(cQ(Z((4h9cQ}-_!)kg}5hYR0*bwbItU^emF=(OVvVs zM=!ll1A)J3A&mE%=g>y$@j&0$D@oMPAA}

INfF-G<;w@QL_*=*_+(o4g znFmY^{1xY~Zs;JQf)**T7uKCk08L&*RV8;~y73iJhjYv{Oh*RCaZCYK7RV{b_9|&k zX_IX-9N1?iiFwU#ldxGMda?1jccfHUV>z0~w>>Q+W8My|zg0vpKGQp9mB1PH&o(N8 zf`7MF3?M1;aMcSW%N%|XE}>H)LCj5Ar|}7x#Yr$QL6p}_RwSSPyukl--3?qL)Waa&RZNFE-$#rJ|J)w3v3TwN2K)_v1#9jN(aZ2wLdX8f|%{eL`c|6}Sh(?7`wf6s&d z`(S;he{u@`btJ^Z#Pl_{{I5nrUxHBAFIa!`Nljm3N@9K>{y;H?-C^n^QWOwBD2;}a z4oERzV=;bW|BUi-umvLi)^srN$YEI*+V;?<@{LzpCYS~J3)4^S7_iIz3)3$;s(HCd z_MFNpjvDBp2n@+dMI^-C(s?Q}0E2c}= z157J%eVQ4vJ?ttMYh{KN$o+Y_VfzLJo>EUU4Wg>IAD19OXK>p8l^*M}^4_&uVRWG0 z+v;L9epf*`TI>7Xv-A1~Ci31fvlMW-SwG5rjtEelMSrUEdVOhU*fyq+g;Oh^SQ%(3 z?lU=>H)&(hh})ol^5pWe5c`=hYEu?ZMP`EP6HnOPYT_n)OM&|A>B~~S=?q(mq2!(p zQ@uaafDBn=&=9u782=33y){}xXC=Tva(oH=u4ZBy8TA*a?<+xz7cw8m(^0VgJ*hoR z2d>|4DZDa|+S#NsHvAFjcn;J{i?3N4gO!^>1cGwcK8R>6{ys=2>EW7w@@)d zAZE*-=mhyMpgvog7GWSN;G}-e0gwpi#`!m>kE&`yBCnx=VZ*|%h+&>75k()}#w&g^z+0MG9h%EmD zQIu{l^1^-Ym@thvuwZ~2*_upi3Efsk7vre?n^OUG;-rN}aO<3KiO*cds)jW*(PrSx zp!OH8&-aDv=aFA%&#okp2iMK{Or2=wB1ieg*t|(Nn+_jkKqDv*<%{bJOX0;3iic|X zn1d8H1L(y9Cg72N)EcC^$MP#4a;8BGoL3S@LQTfWujvbJLz}Smmt6m-tvpJ1SNKU4 zw#Hd;YePe?OEc`H1s$mmB5R|`t}B>VIZz#kg<3U5QWlz~B}HGPcr9s9T>eZQch2%1 zhY#JJk|jZMB3^xO`MG9GvdVvX2gbD z$qWfYoQN-E->s4f@>NBub#40tY8rD}_*pTbmd~FlVo6J40&|4273)Pk8=VpFMG6mC zh})Gq%t5vGwcn^mH`p$g8kmz{5P&hfC~h23?fNH(L?I?XV)pSW*j>PE@T(Yyl@-DU z^#^iw#VSt}X~psj!Sa$R5QfNKs`jOtjpVex9*D)*NVa7yBDpGS@j_pFxbfU3oKXU6a zd{iQep?F$#m=7f+x6s42l%5^LLcP9N>OCTt!*jX*er9i?7}kw{kF!PfX?qRDJ~O^f z#^c$s$2)Jv-=t`F7d)hTCHMwmna$2d0ebJQ-w1sNw66?gaozNkl&f0=VOnt ze%~Oa2C)k6MzE3fn zDk0V~b30cQtip001z;c|^&f8(;gFyt{5oI&-S(I(M%l@*GT4vp8MaE)F38Zv*o6Z0 zd)zLn;8v4+jYR~8$4Cs7J}3Cm(&Xy)OJO6(t>J1#HpyexO7DBfIoK0h>a2}91nz1^ zd}LuZ@I;poghd4irUUo&xFm|6)e*k1v{q�+XcvesQz!mMblQY72T@UeKgIi4|$O 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ⅈo6y|)o-*@)8a5K&2i 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�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%0AVz1{C)y z-?z*Xi<}zv+F23msKbeT;|DH&vTD{`S~n1*>naT{tEQ+~Kf_WVfPVo^bZ3+An2BvQ z0fEjjz3BM&jO)HXus6%%CAZ$;{5I4h8^uWrc(6A+H*hy-P&OfqY7O_WM8x`eTTqYW zSHc=t^2yQOW$$&6>(~|+`dG0O$?0-Q$IV_bjH+~;yz-#43y4$SDWFdH8VvFuOfrB7 z0;>1{%4mUo1-?CAOz);+5T!lN3Y66H!AIgt#Tl56UI72(;?r|HR7sC9FdIC;RalJCioX!!#==Ro zD}pgxnPL7iVi5NMZB|}6tX18OWPXR3v=2;oQj3wJYy({gWND`cN{9|I(L^=LsV~+c zVu-7e(#S{3Du}DWxXNq_+{efreY&{X&kL;wO8poCfmKG7MQz>fSDn2jCa6}VZ*nIE zw@WfzcJ*n2k0x|nyLQ=qqMyC;Pxb}zP}}|dcW?^;agW>=bd{H~&o&`(H?v(et?#7X z!(fo>GI7>oTX}(qpkJq%B!DkG!s;BoOKjR$E~0(uVJ@QF%c%HbK$WsvDoC)nFA+9c z);R&Jh6lGYHP^1QZL)v;+b>1*u__G|Kt!h@16L9{Kw~*i}-)8V(h> zpj{vboz4@pLC4^9%G@%(=b094r~&Z|VeBD3tvkTN0G4vK#w|Ua|4K z&G71BpBq*8_Q6&Tio@rAFRz2SZn{G++zCKgP+ge|hFb4m%movX4YLk5Y0{@rh)ZvY z@eThQ+~1?A0yWNDIY?!^ z#4rIdw4qVJM<;0LqdYGDtj@Bb;o+56m4BC5X)_(i0hT_2glIxuHwrAID`Gn2DDC(r z3SwLf_OFBtx>jk&xn2hI^t+>yeTAZ%m*|=c730t z^u(bY=Q@a*=3*SkWpE;&sg56BT}^w=kP;HrR{!-5qm?3jk=E$yD+9e9vs$B_6hG)BvOql!;@3{H{uPWbT^}r$D34P803D7UpbVjwfF5iF(&H-? zX?rS~Dc)a@P-X05sQQ*LQ}G$DZaTq&V7)CpY%zI_>}9}TtWk+3Q8vXWz#4*E608~w zQP~U!YcbvCjMNC**VEtT#4JfsK(7;Namq!~<(#C%p%XWc=V1ycB9wpi3H*EnX_se_ z86lg5MY#cF|X-&o2)jJQPEi%GfYp1ivt-SfHjMiBn%6iuiC}g8>B*zc*vdRT`m}Q zoZDjqPfh4NckGHw26v_j)X056uQ%;KjbQh||2fN=R%rtr6`knj-7ikdyM*l;2`jVz zR57=fxIwcosX{kDrKpvg{lU};{QzVE(j{-wbN{XZY0@w&+rxb z*jtm|4>WhREKc_BrsqrO(zLJJwhG`%l7unuzE0b_zj(T7Y|oy6zdRhTUk)0w`RUND zU9UH7G|2xsJzZvZ)kYDLPNAm#Yo>;2Slmd{+CHYD+1Hd=+Bx`ootWLN`P*cH%bmHf z`Fv&AoQ|uu9NO*a;_J~*cDgveJ}f(O;+4YKl50oyXVGGWBy&feDZw;W_LOFre7Zzm zlTr#CnfXV-jE0>v(>G`9p%-Z~HDbS0E)`d1(vqx20MA)?B>;F0VH!3HSeBv9?O6d0BsahNC<)RU%bR9W~@%H z+~1>Bd{3`aA@21ZIP=6DvLbQ?QeM@NF_@USAl1m@4am#*%(kv+N6Y3hLE&-~D(Xz# zni6CLbQ%$d^gv7-5k&y4cB9VYqkpaA%iQtP>){f!m4o>oTZ`o_LjAwM6aXsBK8l70 z>&hoPRhPE}sZ!-*<7-O=;A|yIXQmOXp!fz6p4gzf2K>Iplpd9TokB5_G|0mgiE3*& zhEvt2-$atEwJij*(SGtOV9r)!p(rR|MSs5T{WDDQlapmkHSd9ZNln0Kdc`c%^$A0~ zsVwNI;@xl5d+w5U$dn#od1nk$3$`1PBL2Z|(W?YzoCZ5u<_WEgC{ zU7YCP6?jMW*};`nlm5H91xuW1M?>pFlBq;2J$MuouQG+UVvcQw+IW%=Z(h{RBL%?CiG>5= z_`^MnL7S@BQCwQQq!{AoEolO2p6V?wPwoB1Mb_B=*6nA{@*LP3TVIL5&1on+l+ra` z_b-9db09nh(S@p~tXVZWMQQ9xgJn>X#&xo-$MH{~{F?^j7CJfaK9iQP+@5PsjdMCw zo8z`UYjCd`pdZwe0B({SsIdFS0lJuR|7si#7*$q(3#uowK+0aK_k#{rWmjIOg{7~Eq;5KCrQruA=#n2ll|6XFLOAJx}6)?dN#?0Fa2vlN* zVP$h{Kn@5Xz7>0{BD}fdVjkb$4`2}r^M6pYLq-wrJ$7VpHFqqUkW!@m6jpfWA=O-*f2Dd6f{I0|l5YApOEsf>duf~qWy`YMc6}O> zJ$C4%TfcDk+=H;^B>kZ6)ZYa7a}`jACW(Y9+fE@!dmj9kjb>#37=!7Z?KgOJ8@#9h; zdLFytw=M>lbS$;GZo-uZg$cM(dd_n;bvTIbwqe(aw%ibZMOIpaG{!wdf*TA&kT%Ms?+EpM3>_B6jH5p@;^cY!m zZd$)QcG?NYgQk4)oclLIbXJF9Xu;0XGMq}fnUpyRYt~s{@X}^38dF#BBVUBWCM>`N zaumx~g)IDosa1t>0kudrNTT2^#!ZL0y#0=J?bF|XtoYyx5#uZ{>2NVr$H{`~!rxZJ zv!#}hf?oZA5qXb;?5Rn9jBY|2zdA>1)$yWXmBPwszrzA9)l7K%om1*eu%_+a3vU_{ za){(m(+UgDj*Bb^x0CK}xtC=V53LOoo`Ck==&=&zMzVEyvFJZOxmdQzR3L{iJ1{RP zKaJ@|#EC{|tK-lbTkH_uaLN>lBiQ{&0$OGD;wym)=%}K*G4GfjlZx(D(UqoyQeWRx z%3TEjgBVe*X)f+f@S5*%PceL90tOO54cVX>v)f_oPPEr-76`q=fK{0Tu^KkYA%=!u zAY#Eap61%889>?4U^N(L$P-E#*8^fJ^eS2;;)m&yuxwOUX(2dX%tNbW1AOvYFRHWR zQ|SiW>1BBf*_i+~3W-WjJNK=831|xTqf&j+>qmfPn2g7XpgrT@;p}6vy}dQNvu(Y? zVlTYP{ycT5eY@Z)sfw@Ab}t&OYlEgil7D6kcaH}hWuQ7=&+Z;(^*LUQH(UKBVw)}m zr5@%fDc(xXq7vz@!`__w&d*Wkq9X!2qB967II`)=Eq^JDsoC`5@#1gwFA9fo8waxe zWo^P<$?mj9Z5L>~*b-YxlY-FjaW?|~TIA-a3<3M8jpYyog_f`)1wj^}KyYI+PuTOf zSkg9zS{P5xhuJxdb_DD5npEx5mpm_+dU9ib%WB@N=3hLRL&qHaA@WPER+!jsDN0Pa z9Mx{YTR?*JR;Sd$z|R#!>Mce6YENN5-)YH=vn!|#%nK@qONemVUF>Zf7Dj%SPv{9T zfzAfrf%+obnG)F24{tzd8N-x@fpR^P9s^(#&AKh;JNc%}jN$R`;ZJ94fbZ&bk0+qS z_D4U0>2f=D}OU>8i5kp4eLYqLmmRyRfN*n{fZY) zC3nBGvKAF#lHWJ^P?tWNP*;@Q3ZPWHBKclMOx^?#yK^s+GDMC4P+DCbm>>m7$`5>) zKR?MCiW_wlAE2NCdY;~~>M$fxbT!H$eu7Ox>JQOEGV4$TS)ttM^5IiFB_l@P_HZ z9#yhN*MUXPh5>U3dYx(rxC(Z){*lorKJXuD4 zSSYWG=x-*usPX*^x7YN*e^o3@JR(x1ol1y;WLA>Xy(#bAU+W$JWfmo@=+(8v7}LUG zGhJ;!iLfCj&?_*ZbFzED8}weRbTmlqyyOflF&y`6PUIgbF^C=*7Aq!cRbkeKK}Suu zQEImgDzw^U;4D z$)2JiNuHgF5gJ>#vT__MRLx=h-O?!LIKrm5MWVP9A4w_hV|8#m4zjj&LZs?AyeK5e z3P>Ip;Fs2dNZ!qjX_TlJA%9xfCW*jp7_mIg(nW}OT?HZ_;+8s`&v9M2aWvd@L0caI z#kRBe7>(L;I|4A2Laq+AaScPY(k&1^xzLY8bDY%uS2klQuwa>6S(fK`fc|=yY2~?65r%5YO;%p*02uuAr-lVL$Fxfh1ld)vh~W{CW;w zx0!uM8+mIy%W}Jf?1}j8I;>B5E>?=pPS7vLZd`+Ei{e%67?9phX9Jtxp|a1JzQlaz zeWhK!3xMOU$_-q0=07730zaaW!*SL( zMDz3^iG0+4aQG@+180>ADnLZ6455mzS2z@`ERZS>Gq3%{2S|)YU$tz72Uu*$aq&Ef zAwgqU@<-KRHofbgDG6K5$cI+yf^Qt?E8ajlq~&b%ABqiV_xZl2TzXoHH1nBcPpuTs zhjg~j6IZW~Vm)Pged9#kA+3CaX1pSfSHIU3-3q95dWGLlflf4Z?%K^CucK_VGkuTY zSS~lVa*k<-!Im$%#%+npyy~I?&o#%KoWzIabI#y89j;^WvTur?3bO}nzveNbZnJIc z-^!h`+ehu5#7?nWj383}X#KSvc)sl71(D2l%~QUZ3Rqd60qHdjwMc=-3x6#R3G( zk^M=wvzwvP`7>d(#YByb0MW_Bw}|~wB{fC(@NQhM&~AtrzHh#3oPXvgHVwz;O`YZ- zd|d0Q8Vc_S)bm1r=T4WWEbw?ZM;fAMEWfsq`iD3l_N`&Yoc~5xZqSiPxCt)RWG$;n zN#_skxPn>pTv+a0zC9?^jik5yuASlhh-XuM7D|kdRyuxv421%~Kc>pN5gnKG|7z0h?7?pJ)n;)h7yF$4G{9 zI2(v+lo)?bB&dUqyC=@;0)|Ai@&&t4f%?`WD(nhoDafU4 z*Bbv^ianfDHnOEE&d^$qA*QZLW#HjA!*Tx>QOTil;%G=|0>^-Ajg~{~FQ~5OgLf$8 zceuc%P(iLZ(*Vq@RAc4^L(6Xlhx~X9b7O>dm1t1BG{Hp+`0N|Q9 z9?*l(wf8yVyWYJ`Fo(^!H4keTn8X54;#Hu7-7>~|bR8qQp>b=i@EB3(X_dWTGb#z| zVaLIxMWwPF>J%4GQ zy)7@{Ct@&}&-FbNp@zL{?vU{jfMT*) zeqM(Ia7R6PRnxD_oD$4GJFk(MA|&)e!BMy{PGJG14P|Iexf} z`O9TVK}5h)dw@IyoTI^pK4OKJj6uUvQWp2`Vgb_UnnEbXCE~gFe@y7ci=tzVsqwBC z2PVn{7t-VsJCEQ^L2z^TXP6|+Lkz{!t|udqv-Xyfs9uJwZ>G-o+<0G3>%Hr6*b0Eg zk%BLw2RetI!H(1>i%ZUWUAyPD?s@YQug2i#mB*a&Rf7Qu&4{RV1iRXR2xI(k4(zU; zQ%Dmq3?9PqtTE9+CBbp=2^|hzeFZ$B6`G!A6Ow&dzEDvCnxwpQIvWbZ zg1Vc8?Rw21H#Vy7WYyF{`q<;E1|o~m+6!Z~)1LY z*cj?Dj8iOTxH4`i+P=(%-V0Yz41)04**d^(-*v-`fhbR$Jd3swCtsM*&Y-f-=}Vpr zq~vf8RQ~k5*egmrG-HUOtjac!8jl|#V-ST_PBUs9VHA~CPD9$g9OIBo`dzIF(}OK! zB)NE;QII;$;PJQ_TM*@|e552}mMQD&sf#_8@U!F@iYo-W|7M+mHz4?$`!tbeEqFHH zHs0F>C5SACaw;k7AQMA-as^I3FCat^cn>H+$Yj>geLo{7VXx^ zO@attoMsv#m2N?7nt)NjCp{uA2W5t1daje3d0n#dunG^;+;DP0|NJBG1Bk8_r~Gfq z_J5^-GyX56fdB7Qw*M#jo8^C!zke;9tOWn3g(#`_vqT))|58Ve!ia$wr*ZKyK?s$rG_Or4*T-+a8do3qP<^u}@hL zF9e#>AuFMCT4V8hZ}L^2T-{mob!XJB?+5#4%iGb0I8C7Phb1*xS++jc&;#D3r`i> z)?*)XJS5z%*?hKC6HAx9JAn?qwwk(**`gWcude$V6fy%J<>YhgyHWp)fCQi> zT--7bCaO2Dblxc)TJx5;IlD)x^z&X~NlfvaCAR>}-US3WZ*oC@ofb;I8b z@clf;w70ihqT<9UcAsfWPmo4+U9wGigq8{NY`ChSGq)O`rb3xY_NehL2&%rGjR0RJ zl5%_C6`1TUgQL?|1Ato4&w6OI{g)Xe_jf5Rj}*_?I#lkO+!su6y zVtU^X_*V$p(#LyZ!VG__NU=iRaSRtqU399#?w}?1%2=MZj;VU{)&?BXBF3?{3HJmc z%oeNSO+Xuc@PsO+<2WFcH5kP03*2ez8A;puLv0JDAc{Qqxa)>xOU+CxzksV+pCJOn z06NC)kJ)6Ob=;_g<;ZAmo;7TArT9uo8(k&!x@pd;?23Ox^ZrIM|4Nuf8W11VyzYH zd&ih_JVPcmf%QROO1e-LjtLZHIC^uc79sDvqWyCD>aVb=d@MSI(7{fr!_6R}*kb9U zh-%{oxs%p7iI{AO+w1Qa;J--OBM{%^Y?>MycbRrY_N~k}Zdi%G%!R=!zkaKEa`|n2|DuHW{^R#tFc1HdhTEv`^#;kCGl1K@1OXh#hV$g!nNEwb{RS)kmx->w}A4*CfrWj&MZ_xPq9Q= zeXcgz_-hSk6q}Ov^38*QqCgOLsi_4OZDBva!z^h25cRRw{r9n3beH@HW6{gS+mlBa zhwR32uF69bAzpepBsV%e4zMaq2}BKp!z{;S3|HVKrsj;!AU>?L=L_70)V)?8iW%|O zqq2kUB-YBrTl$jagbFR|GI{x!s+WRkC3XA!}mRDaVDZm?sooXviK)=@w25O z8tzarTx(|jualsjtE!m0^XJ&JZg=FmJ{IeAm>!3Ino8f7uhl^!n&xgALH#XboSkFj z0j#EOV_0w>aT!W4yqR)9db#fUva<>;(P7HUF3z@WLfp|hxK&*S>!zFEQZUZS@r$WU z3MRkBGQXhTp=a(#uety!`YU@eNBJuX9z51VeR|7dXq$ImOvNjwLRt>pJU=Ab=IEUl z)Bf%C13q~dH=&W>RM&_hd2zb7_|s5Fh_t_`S6eoqdT>sSL& zpRvy%5V#w}yNR^}e~jhEmwX;*44J=lej5@E=yL;i#o1!VmP~M^i@UNluENnSV5du8 z94<)W2=xA7-zAE+Cw#WIq@oYtwe9XVcuYTy2$Nma@DKIntPPeSoFp!S^x)yP{5Qia}{AgEZH<8SEN14!(*|M7Hv)3Z@W^?t%MdkH+?Xh069mS0zP@PzaT; z#UO4juFd=(kMHv5*1oM0%OJ1D)Ue%vPs_!gp+UghVCAKXzYgaTVG%<3a1d}6-u^;R z@x4L-@Zvwu@z1o1Ik$)t1BiS99Zv^;XT_uMQV}eruP`%Jiu_?%|*ENc`|5`lvZAf_BromPFWWcw3n;YDff#+A8)Z&; zB-6sr$_*P&c} zO>r?OkcCer!aJS6UKIo~bW7QRp0rH2s)Foru|*#pI(RETEGX*kzso7(n9NZ#;xWW# z#=J2|)GCl_SXH3jNVQZDN|{S#J%i``mE$p;9g4oe)~nUIcE|PcKAzX<7hCs);DX_P zl8p8N#7A8 zgnXtK)!~;?;mGyThf~gysLyz~BAVI6rhs=@zb^IKv_*h&+w%8h7rN=4C>e+f`C^I| zCX&mtdHP+=ycJ;f6185JRy!f%aa|456+LHsPQcwZ3Q>GmRF+>-Y7*9?fnbTI!(E9Z zT~>}p*r5}C5r4$Mr0Nqh57cCG0Hvp=hZF${uJ0h#gXTl{hq2Bsrsk8?eFqdgY>BZc zmJ>kx!VbXZl2ZVS>Uu)#fHYUX=D8AgGVhDm&`)KjbElymb!+eCm}yer^VjhTX^TU} zJ%r0nVL2`UT(9`w;vmFfz55U!h-|IsZVCe|_;m1#hE;FVX!qtV{rD>L56WR*u^Ol= zjdX|&oRB8)(i6jQlcFsaDBF9@vp!B(^M?bDvM>q$%O@Gpi2l1dl=lcA8iVW5wYgJX1Oatt- zG7EI_;*QE;Mdpr!VpFY)`i~%sr7rkjbL-Y=%-+b)rd*05Bdr0kq-d7P29e~3MCH@CX! z2K9!Y+J#Z2)z*@y3i@V^VF_Usx2G*u?vsvEniB$X^>4dp$ zy@z#es|p69f_C0&SK+Tv13V>xIX4%7DR`(h--)>Jdc8+V&jki$l61w=lEqvxvkeP& zvCBbRM{w593-$|&ioWlKn_#1^caSqjLmhnJx}AryA=dFY2wTC54K&PD9AksW!i=Pw z6?>3sjc3BL%808W*{7>FP&+atuMNkW+W=l6qn;pw zo&Ymo8+P>1LtYoj4eZmpi;e%uAKos-5yL8D;Qqz;i;Tjvc#vZ97 z?Y-Sfs^$=eT6tUNlLR34wedLU-U(N3Jpt6@;II+bl|%-X?1Z@|+hr$DwaqsW_SMTO z-K$?2opkN>?2zP6T8grR1pV0#wXOQXz0WoLu%5X9n6s_jJQH=5^`=g|g3 zxqJW}x%QpjodEpg)9jbR%{3+S7+8?s1nEy|ZC6aA#JXL^{;J(|?7)06+Pr#(lyZ5- zD&1~pf*Dk^dTeHXZ^GDa$i?nwrCc6b*oW}N>6T&jWTmy68H&?Q#@M`JHYKBA+k)eb zrVi_VK9*CXlymi@9-WX~P#+MxxEdVrAAqTN24@VC6hhhc$b9@YGbwbq_X%yj{to&YO&VW zrG-f6D79wsG0}3(0iHoJ+CY9-=-30SW53-GLpL~}qI}Bfv@BT(+6y?Ua;=haT2n6b zUEkk=%koJt@BvjtbW$Vwu6rdJl_t5gc-*1)UVXyzFZx;G+%83=VnMoe!Nl+o3XhXV zZ!(t4QB;hDwsSMkYe{>x1e7cfPd5R-m;+${G?ykWb(JPU@5LFor)5$K`^ zB&W#Ai`qTB(_uVqW(BQ|gK#N#cN#a=Yb7*x^2!<=4Z>RYS5FqCM_SbO0lg}ZD=_$} zEymm?>n`uXMLN@0Iy2vfSx!ju%Xndeyt8WKWN8Y7(GMa@caLS+kl=@5kJ7O3B5-cr zk-HY$3$Hp|Do-w4C{0iRlxphiRWm9bCutHbiM_luViFO{Faa?srEYXzzrZfNvd&s4 zpOy_#QE6SkMI_kYOdTbFT4zzDKbgFzjtRn5Ls1AT%<@m8Oq-Iu@T{FJocSZL(@E8Fy6HqJ2oA8efYXP@E!oyM8}t%YFb ze{GxoFSXAwGBN&__L;(ew@sI8PFl=qgD<-zFarSh3=!$}dax|H|KXd8-_=?E!#8F7 zH{bM#KoQFnR^80SdHHs6RqUFzW4L40YuM-Y@MP15ErX95{o=_?vwAg0jEwB?&x&<6 zbb)$t)J!mkb&%3l-Gs5=8DWUwGTm>N?IkIf2OkbCYPJhA$=_-X@575$&+64HHQm{n zS@ol%P7Y2UG^<|Y#D?py77ol=lEo_zHiSWm+5cDvPST%OQw{Cw7x;x1WRy0!^5E_; zZi_UW5_Xs&d~dCIGfcdHNU4y{M1s_hD)p85Wk?v8Bdq!XU**mCxjX=A8F%W$unWT` zpI+=(EAOTXbt%p6Fy27il&M2nw$~sNZ^8TLq>i=EsCc@rZs59Zp|nJ~d|0f-9V1UV zr!1Vi0e_iF>%Nl`Flu=K!Tq4{)YoFtD6jHI z7cZ9o#&SABa3q@rR3_+Y&78g>I7m^US(%oEomH2bF5_&35|sy^K5}**i31?G(8+Qk z!7zE%iH#LSRPaY)jN$kPe)d^P>Bcd#6$QGAkgh`8IXcUv^+zl@%cH zAK@mcQBh2xb!?@s&yb`cOeF*!M(kWjoWg^eL~hmHw;Eamiz-W@qUBKsu#uDOsm++= z8a}}f=C-W})97cI^r{TGm0{Lu@yo`N)mb?ZRsEDng^8>CH8PU_Zv(u9=8farG|<=; zSEB{PoOe}qzG8b(BJOkWfQLY*d5{Mu#K#!cHGx1^6@@$N{VMXo1ajG$buuky4}sh| z$UP0;khak=B*^jPafC1z9BKcx`!tEF+v+KoPC|j~Mr4N7lMHVCw14qsJFCBg<3w~s zBVS~FFm4|3n5o3ss{{0*RhK)+z_R*)i!v__95T;98?&o87N;Ygg>9DD90y2;B8PF{NTTJ+?Sd_(hysWH_QcK21mvOMCl$OB5raxab$Zm9 zM5fB%z3)y)MGG^@)BF*7H6zdw+5B(e4w~5lfI?lbf(U=Ds5qdXf}}5A$t?b^8Q7Qi zX3s|5(PHujyTTGZ!)};YBG*a7xbv@39^1Ui;qN64^jY1s5@q-hcFNO$dRW&AbWTW! zVWbP!lS64vnR#^VKBzi-RMb7Vw1u61!aY2A3FUcx3syb1na>x44F)P` zrmqyIsDrDTecO0)Yl4DBS{B}Snqog(C40MDZ~yKA7*fH|?nLG_4F)6op)^=YtbokCbgoGLI>?63BQgrVK&hqs=_Rxgx(rqi6Zi5nqBs{ zBH*m^_REVR%BVR?!9)J_>bYYprr)7EtqSYl9+25L4CNT>inLw+fV|}zLQVkghk{fq zbFWMbRug0gAQ>8jCSgezzN>|b%TGY>m2ufV;|ya9(_1O-WhOaCEY>tfgn{2OC{a!sO-6IosYDwl|(P+M^8zOHse&|Urs&D zZ*{Qk@3;e4pD~Mk3@UbJ-SXKfU{_h7=vox1X>Qc=jem@(a7v#Br)eHiH4y0BWq9c9 zFHrsp^G9z7jLY6>iJg-wGi$Y`WHjcCKD^lzcg=2#%0|M6>qf zXN&OJVq21?jx#O^>F-E%KT$4lY(C<5GJQ{of8H@-XU0;i?%X=OdCZGOH3lGnEiOjf zGDvT{ohI#$3`8P2x2Wk!2nLd$&(+O|RFHP~b@P=JKyd=~^cn{bJyidSj{c2mi%?M2 z**r(8Gw^rLA1Ur{h;MYgJ>z_uZvRdA;}_*NVXWJoVdBXb6qVr`Cicl_Az!m__(q1I z(5jrAGAPy%@RNLEcJq*32bDc%&AC<6!b(FuM7zco?ZE(NVbMzk%Y|d$t6EO9ZH%yY zWap@qMD-E4m<3y@$&*tP2gj8OEQJtJs{LnCs(I$6&mRGXVG_n4^mfrK}oimo?)~U5Xw>RC< zVtd{_V3PK;hArDAj!iN=%Jh5PM8F}OKgub$*--?CHkZqQH_zTaFu zP}AMz{VluEsk5>1*E4kB4xf+!Zv5=XrYlJ;Z-cI1J_GbY#2mOSXg%I5IOytZ5_JZxyV$&$ci z2$mAsh`WBeUMs1^#aOPDL9w)Mvn}TSJE6j7qLL)X_@F_U@+IOkWn@yCOmS!OGb3&G z?(5#{T6tCYxupQZ0#9h5I;`NyenJ`L9t#)WjFljV$mZ*Ody1d`#sRXbfpSdP$}q|J zAaNno+r8myc+xfpg40z^@2!~^_xu=9u}UfUi$#9%rQn`dk#FJgmZgvuZRodosF0Mx z{95^L(g%)H;(qn9tJ|cmF#gGfvqoOjGo$w@Qqmydq{@-w&Zd=(@u%QU#KATbvI0E~ zNnhrT5JA+zJYp!y@5uQ+VFeed?sO#J^J~+4^(8)XPcJl$G7BKPv+mBv2%j8tDk@xB z^CTn?__hFrFT@;sUz*@cAIwwn)t&8LRzB7YuVK9+jNif92XCeMF)CIA+EiJxq}wyz zq|P@@5dds1FgmmegTVkgV3_RF90k$~7um0k*R6ec;Fe6;4*__>G^F-2ii=Kl>eZic z_bw4G;4*qPO!5tu`BoE>GL69WCszX?H~ht1=m_oXpuTxAm@nTCSI^Hz1bRtbEr2xD z_EC5WPb6(BLqaS847Q3?>k3ogflcglHE!@QiB9Td-P0$g9n^ycN0~-0R8nrL8-^dk z5!AcUXgJN|24uLtXc&1#ZQQeZ625lacfaosodtiT!LJ+utt1rwvD?JiaP0Ly2ibHK z^rGn4@n=?gOSXU0Qe4c~;WePwlim*OEsTNPai%Zxll8wt-m z^ry~0qz>p0Xz`72QHimeqoDXr+>0s!y>J4gNIirJ$P)LhXBS}JXwZd-wl*^UF>s@( z2_i}~2{}64$l+KAAL;k}hD+FXJY_w5u<-7&UT2(G7D_Hw2b124$?x-L(5&@wF{>iM z9{C>sVL+W2a(ulVDj>B#e?1bFNi+(il@XL&VmJCrn|tGLA7L7lexX?=JwRR0cs@U;@Q|Z9E&Fp)B_<(5K4xzw{+eh*zhvS+8+eyBySUU}eWg z3rP~5&-U63pUj)|d9QO&b7tHx+iaape2!W*pQp~NvzDceP&fHuA4>(am}x*fI>(t8 z7e0Z}-zDT*)RRt`ONmPSdx9Oa&yYVvIX1vPjbV&w2mN(q^jrI&4@j%TG+K}5WYS_b zOnCN_0_ZY}YqZ|tgGho?s6mtWlS*&rQd)WVONX{XRgho}xE|yjFT1>WmGYEW#t#E@ z?e+e8rpT$slvJ87BpF$I=U3$HeED=a;OY%D`rA?eT3xeDBekD)D9l;s&CP-^cm>ga zP-eqCU*c=ns{$cIRjqO}IMnwA?lGXz)Ll64RKIByizJAiJHEjiuLMf{UG+q5sKJi| zfb!Mbo7>>RRrkDklDTk}m!vt(j$hZa@oH-CWst@i$L8HFh2vb;p=?SELNl5KSMJ;t zbY*MwU?+CwJC(F`bLOBGlsy!+n1<_$pe8Z?*j_4npir}3d>8`8o`E~LUrIo6^wLCW zAN4f9oLaMpjLZXzSiAc`b|{l!&qo0TMeE8UZQeC|Qw5VCJbxZZSUkMr{%~X1?td2^ zkUb3Y+p-CEvKNf)@e`vrVGK&TgwnF9;>WkR@Sb+@D6qw)p${W6cywu@=q-9Q&Ef;t8)xl`{`LL^rqb zmn+gyLsQ684kgGFAI*WG+@LIi4lQsjl;wTvxQ_Q2oM|GWvw?^vn?y92Ob>~#i0#af zQ}uHMHp-M{hcT^-<4my3d?&e^Pk4*T^M#HWyO|nr^QK zKv~y=CRtT(Fy7I$;#}%Y+@M3D2HNI-(f8W$1p^19(qNwm6a@quS=g{loGn=4pm>St z?6W2@JY51H39t!5KkSi!u|0&kPoL71sP0}>IQ06UTw+uf_+aZgA4_~6!v&JZt z9bWyIby^?(o1guX?<0{lKpFnm9hYE+RmB~`3Cu-h#FFPJnGlXgDwLs)Ok$1y176ls#4;;aiV0L{MY;!ZFz(Z zRZ!B`D}WrO@Y~jaJ3kk*ona1zg~G;b1L8^bHF$Uzf*#K4H2^l#1ErnVuxpl94UTB6 zS#=Fl-P-~2qC~+Yp`yL`Ua4DUNZu(1F9Dsna|=UM3&7=J;kM=u z4W>VoSnw;J2n*kFx-=b8SOPcIHK_%iVXc|qAqNbW#32BXOL2bK_5A!1cWag!4#XTo zN9RgBbZ|MzhY=!DnWt%IXVqtxeUHQ#^m7MG{fYrJK_V!QUAN|IJ;V~-qhZBZTMqKJ)uQJgzM{V4 z)7osM5(WU;8%mk8R62t50V+<=MuDZHec6@sff>~*8O`w)NC*> z{Ji<0cXso6FnkpK2XPKzZH>8`OQ_6rOkQS{f3)kd+4Elu^+lFX%1@fY)m=;yf{4sn#P21JJBklR7OcEd>o(~X7Q_&n#Iq*=dx2X!(d&Exm`1Wqs_Kp2bw^I zKYRyg0DIc@fbHq!hM4Y+Ki*qxOy$!3AFgfEtA|Kfb%%%7xt7^==&H5b^lG0_vx})t zt^P165~`Hc(g}ix82GmoBtI)2kIg4p@|4>R8Gw?*4aGSZSd7ZUo`W6dAC$sAWU!VpKZpW2rG-l6(cCZ3gV^{%D4qcGlWYo z36X{bD9pM$_sR*1o(Us?OR{^H>u0`>f4ImYUn@0f{%JcAax@{g%DIs(rEqh?%f3)P zIzZ$N+T~0p%=!gy%6mAHKN0pQxj%B$8FN0o`NfrtyY&92VxQ-6W76gkA>bj}P4b|a zP^go_E1Z`%5eYnozX2nt)8CwM&(K!UQGR6RY2}LmSk*96R=qdRNU)Il1OntY^+*sE+gm%VxYmNz&dyBt^s#ZjEoE_C-8f4|F^&j;N<88J)&PJsFQw-WrPmPMkc?>ZWeSy*+8e z$DFqabSz@eY?Cj2SV0G?2&P;*Cl;3Z7)s4FT?3ex zvr~6^r+6<6I@`JJ!xK6CGC^NDm5*uymE1)v?xQ)btdT1Hr&Yb@3(wUBk5ANBqU@#yo65t~5G>&P#C{V#y-Czp?^^3()Sor7 zgtEP1jQ@#$EoyG%Wb8mEYW2g47B)7tH8Q3X zHFt1y!e^zEHnuT!GW&6C=w$K#of$1>V4>t>{U2N$vn@sjdL?@K&}L?OAVmSFWUyj{ zHH0BHzqw@jH4NclpdU^V2?^A{8cQ<%C*Jgb_lEvyoe2GhH}p?KN9g|qw&;J~8xmst zP5W`%r%n+QBQyJdd~f1m13%#IAE)RC+}#_qKw$Eczr2~x zKfl?3wdVn$x!=_q!C!Yc;2MpxB3XaB`}IBvBl1`3wwXuUSG-Xi?)WgxG#LFF?$9Qq zTI%U&atOplVMYW024~kd=C-3ql#AmOM0YK>}u+muD-H z4*pli3yBols-F(^>9UGP$N0R1+QG%;`gqP(L&Nyt>fpOvd{vAzql@;jDf)PkQ8-PD zsiOF=`onY@JEN7mG!jBc{k+Bi;rfx7mAfyi!S7vA?t3qv8)iNa$ZdB-ItMxPsl*!? zwTXA{*Yz}EcO;S9Y0Z$WyBt%$8*h~MjZFJsyc=ejbG=-1>9bWQ+|Y z2R~W@^q`L=B?41+$H%Gp@Za7SSvZi$!P{l73U_4WDo zrlH;>*RL@dstZwW3(0^Hg-?daYwiDB)4_d{hfh)K7ED zOH2LA=~ohP*?FT1bh(A0Fj5+|Qvets>bT#aeOputK4#YLr|bpb_lglb?d8n};=*1! zruYZch%2Q2w1n#b5*$ka0)#Ovfb~kR;f6)CG(QeoN4bEP?S!2FCx!#e4+A(f$o&^| z#1s`Bra=)HbMw?*8k{9if3dfz&M*Xn44_YcXRaEc->a*MjQWh|3^_5@KC2*Hn`obx zWtDw7R11HWGQ2)qg-nAS3qeLL6%udj`wcm?RDRt*L6{jS-z;z-jA%2rTtFM6nG97{ zby(qu1-xuYKz2U7tg2UdmB=(ig1S8S11CTcftcw|uz-0KDzh?ICX}yM*DTrDOgW^r zSHB%@Qb!cSOHkRQS=J1I5}((!za1(jLKteU3HCDaltZ(mYkCJt^-OgK$t#3 z?nU-iJQeWP4;dgQH7~5dWJToavx=w;sTl?HLJSp>Si;l_B*N|4jeAq;ra(1&B z(rR|Ge~lo#=RmTWk?xz{DnwX(9?_~kYgr^>C_~V=__t6%Cm6c-BnBt;P#dko{;j!% zY*h)>@-k^0J38xr*9F&n>S&3zhq9hJ9BtIOovBtJGT0A<^^~#fFwbq=i|NReW_npz zUfOs^*>2(Ts{VtG&j6`g15;lW7C-MoLbirlHkEWyap7;&&!*ABx99jA)VI=dJFqg^ ztZ7lbo=B$oiX`MFoMYyj*Du(n)ztyKLFnI@5LmC}qv)zzYKGj8rB(o-O z5gxfZp%zw5|{#xx%Ibp&*5G9Lf?=8(5di?2~Drhs4qG{wqwN7^$8pn6;lsA?=o7rTxVIlgZ-Or z2TZGB%IxloqcOxvAse4-KG`nO+^1MB0H6yu3;l9_yF){tA?6^Hu_a|9~`3zl(#slYlm%q(<0w=Vhx*kJ`x(D!=+5%a6TaL}L?BMct0 za>QK659W9Ufkj0!uzudWN}Q0EgC7Pr3&E}==?xfn_J)Gz_5~KsZrt56S$Plr#clg+96%)<9 z9GOVE?aJ(K3giA$C?{41qNRMg8y>Phk0<59FH#5+2}!*9kHPuydRBW4Uu5`1$XJ{f zW;gaHeV95XeJ+>W?y`>gj7o>;2J|JBDjg^-QO#L}-&^9kPSHKSqM^JaV0)SINc;S! ziU!SOW@tz5HunLK9=*Vg&mu)-W%!6 zOf+#d-$we23XWBud=Dj{9^U8rylLs!_UWrhY{D`87|Gx`L4*zO*1Fk5l8HSgJX2_D2qOMrQolqSOM@FtX)PN@M|~`ig6$g&6HJ`F`*U5$zD~5N8_0v*7+fX zcD?1|NHmO4dU8hrJp!)UOY_YXo#P5EI!-SwGk$%6oe*2J6NKvltiIfP?mE)N8cf3c zHANh=$OSRC5v-ONsx6q+pbg(o8(tRdhj>Njoa+UBLjN7pQ~+EwH|7-czRL9~6gCeFhh>1OxuOWRZ7EYB`tu`JQL^e)w8|^P@IK?ykNE6KW6ENi=n`V7_0`#( z5`2E-&miq69aIL!A$_d2?^(R)>ngaif^tan%7qOx#j?7u1lbBT#iHdt`$kX16Z5#` z_TmiRsjtz7=8jlLJRVC+Ay{E`Azp`?MWsL62^xtvL9Cx3G)lv)pUAP?!z1~q#f zfL_ssucC2sNK|sxp7cg>Ix#+Lz3YH7E2WizI*IF5!u~X4|9O(BK!cSVgNQenQiMbxI8kUZ`K*lQig7crH@8T7na%Bzdx0 zs9kz^SXr%9aK>4ls(@97q-u!{EesP6KR47{cgah#_cX=KT-m)9zk*=<2@7u$$A$CZ zigrW{6vCb)j&r1_avm!ixJa7y`zL-P)oaMH7N){cA^T{E-*G4fS^6&^=F}Ou)GaAs z!w{wOP9N*=7uJT;fh0b=gJRwh3~~>*<=x0p7w`#5cwBO3fN}l3b%F`mrw@GO#iiw zYC&z?W@7}=ds*$lkZGpzO^{WhhaP_vt1HCLq*5y;^Zg# zsH!RHFRq%oii}rn#=)a?DvH8Jt1bg3FGj1cJ_AIp6Y@eT1)4>~oZ;{3ydt0qC#M42tZ`&a|V7;i+~_^+Q*(iWqax1as~{<#H}LM>wKHhAasc z7SqsAnZH;5d`1Z6=+hXX8Xyb}+IpSjKFQbI#vJ|0AzE@&UpH+HF65CalZW^rV}}Ti zYDWrf+@gYS3xe>e%!ULh)C5&_`B2&7-j1oq{{$|)YIhJd*_+{8{*VlKeE2{#uP+hRsanMs#%qWM3s%N!QsUL!#L+B+#sj7YYipi0(dBn z_2WHmoT=C52{+Hvq}p()wH3zO?1vVQXcmwthk-k~uhP<2rBSHL1(-kuMhy}b)ls2r z&;bCRf|K+`3{}Pf;zw5iV0iGo`F8rg1(ufQZvsRM!yxXQL5_#d!5k^SHL`9{&^N5Z zp1rJbU43iPoKTL~JhvsPpn!;v5;~xtc!3Kf_qdidl?-|gP#ww}tr_f2FotkJJwKit zRSM5}eu3uPj?RbQoy&6feDK>}$^cHf#BPbMpo!r`kimKtKixHB#hiS3(C6UEVkRd* zo{8%~T%@-wjnF{%ith0N83Glj(%V7%^NdA&uYVbP0{yFk(PrVC;nswK4;!pbtV8~g z7KN4ig;L5f*P|Z1xtJP!kJl!2aeCw)mhlzlas?(l&AN;xq$Phn{Y%00qG3&*Xz6S+ zxC+^703OmLVOQGte!kqwGk+^z3p6itsA^0t9kk`o{u8`?ePaqh3k;*LJ^%=TFSCt% z3?i2+56}#AASvn{&4n*dmV_K|_LcUuOLIl1UH8UYGEwo$>TpqMyrcOSNHe1Go1u{0hyW3wpPvctE&8Ae&D{bCO(8 z?kEkVfCi&IWC^TC0=V=n%?p?ZewOKYWN4UFJrOS9I)V5NN+W@2w}e7D{dx5=6YswL zNopk={9m;yH^w*a*kHz1?$QQ&fV19#m$;h(MkZAL4sJlN{!C0!5uSsl*Nz?{AP2Yy zIx^jO8W0ps-7ZQoDSE7B2w~9Ci(j;d&&y}^4}*X`zNbb586af`t)knvsKoi@a+i~u zh~tzE*02hMl?h*lup09IjDpz3nfz(2^%7wNCCI;kz)f=X&vx3WGlpijdhEGARiN6u zJ-=pC(Ozvq8qnwzIbHb6`f}UVh%tphTHst`Q5NGapGqfeeylyR;6hjOU~nivCqVV2 zat#UtDLvC5FB5Y)FxZv#TF6)z`EtiL=t(<*@ZvMZHx5&q9 z8!WgU2!7o!*gtB83fnO+?~?=$W6C|zoW$6UsEb?f+_=`Qkx?OBp{IBBC|9Zl@yO61 zw#;Y(MiK7c;E|y9ghCfY@N7jSr6;-HJl%jD=NQqmSuR1DaH$Dy1r%ZKaD(ouq3oUl zN#l6C-xG9u&G}+roH_f|23u7`mxZdm$C5JZA@~osk`nF~5M}026dFmIw4(^r3gHT= zf4u!1FF{8=L@D_W z{3re#`TNg-AkM#wmL6jp5mjrS%3hjbjrCTl(@5)&ci#yll=ao%>bzI$ry#%&KsMIX zB#B>wVejZ~hM5uPcMao!oDc)+2gK1pu2;=qa2aEzEYqr_2>(?9nh_jYv^u0 zyz>p&qc#p4ibUXLse0QfIznT4n}~L*;%H-UCA@KO*vw?57|~UMDVMSeDCwGMMzF($ zR6&oU8z!y_QAJ+>1z-~x&Z!sJ$IH9MyZuNc2Bjso8HSd7OXbnmTUx}?0}1E~?XP7l zE!6>$NS3CuOp>eqVAJaB5zYtbS65(ASJDP7=mYXvBDW$gUg8j0s!bpUWcIb%jt8sq zg^-VdLhB)}i2OLHD_3C$VAm6EQ=12(>##};Pp%)CZc%)`$wZVF^6vqn zxUZyv9|)zx_=P#jBQHJSh!{!*JTqk50fP7zb3tn}?RhwBQ(SE7ZJNL`kCHNQ6oqmp zK|?d`pWjVOL`hHwElzZ%oc27xc`;B!Jj(EtM1T5)Tr6`a_Y25*KMc3B$q-n-c(CVo zYKx#(d}O|r#ssMr+?OOLl^M$G2AaGY4BnP%V%@yF9?7rAz6JJNL6%KxS}O`8w>nfw z%N4i~BhWm2PnHn9@y|aR&5y++dgz=DmX{z~@J9l|q_(i@ER2WieF#LwFA;~Ljk;#h zty1c&^)yGFB!+j|@H+sna9hiGVYK~}v0|-Jl-&j;H+tP5IOn>rKUchVvi~`M(RJ2u6%O)@X3aN`>%+ZPzZ_u3ffm+uCK@wr$(C zz020Eulw}r)Ayb3zHwj2i}x>oWaL`8A~IIwSaXgU&Y|EdWyldcsH~z=$}8g;=4M3Nkq3I5?&s51KxA#db|AvHtSg9Q{xnSIwpx)ELAw{X8U#q0e@Y`pRvkUnKHe{Ypp-RMqjB0(Ind z^w^cZ1p*VfTy=A))m*PsIQJ*`Vttn>C^b2)j!cBj2x!Dpq}$CaqKQN99m6Vf?l)oM2;E#~r$-5duOe`1ev>I)?`eba$>$bLiKZHpPz zaKnwhFA?D!4Y&=4Ha*hkrV;6zj>)T*au7w_O_hBbg;8W7%qQntZ-7B?X{`jTZuIWn z43?o04!H5V)+sW5!Ya`h%8R__`A*G^b);u*P z!y8xdqU4yPlv;{D=EulQtdLH#m5kJZME^ez%HJa5tRy(PdGVQ+jXi}huFVJ@?Y?V{ zPwUIW#%-(hooQu1xR0r1BeC)PcMsUqN)Ons`o+{u)HqepAeFhsMC3z-A&(zLgQk+) zv6&RDbcVWUb5Mzz=**9n>ZMcB^xR5O{pHS}c$t2q0y&XR`MuI8!dX2-h$|3&k?V`` zEvnOXH0It_n}%l#CoFI?G|^4W2giGS*G#?jSSgDDVrNFJ99*h_%MqW3lCZI{-UkZ= z3phUc<7sIp-$557TwOkkLF=3Rh}7r`O~GO8%})>Qgm=4l!|&E-^eA z6(X?>b1JIU*VaI=fAp~^p8eKpzWx8&Ql|fst z0VvL$F>F8Yy6+IfWV|^D{Z&X8(oGAvVYiOjsQv^O)X((6v_4*Nw>FeGk~Pnznt6kt zu`g*AaySD789_W#g*N>_?M5!lIo>N zFGRbe>^1ENY?ufr9w>q@KS8#w`_Py(H-#G}>N~2;uSzSk{1V(Dk~7_i2#+ALJBvsj zIS=$}!@wHX)6#16XIBr6T@?^mEQQN|7ZAJD`aRhPeycNH%1L`+(QK%?GpP#1o*k-t zp+?#@J5B4BxuSp{UrBOW!u>VLwy~5UlRAeu-x=7Tc`&I#IBe;{lrOe1_JJ!76=sJ3 zzE2A)!Xuqza3#6w7!MP+1la(&;dyz#dj73#+p)ua+YL~YL|u>fziY*tj1ot-V1tC7sr0)kNeE_kI0%&3iSsD{0#D^U-!zJaoCoIvqa zdOcUqk_1>~N<%TCdnB_-B;*M4p_PX5H+gLl5#pZQLL_zLO`jYkI5B}RQ|)asXz*=k zz87@kO?VI_5Rs_2-ftBe!WB;^4uA%>F@g24L3o6dwj4l)ktkDIb^dft)08r<{yN}nx#kxPjEGu0CS2Y_ zd*E3)mVQ`0O+aUC&w$F0j?_2(VDf|bm*fh>OvmwyNUprKTGELa3yI$AlI&_Ou;9EugPlS8hF*vX*xotju0<1RG}y5 zo^zCwXBC1!*OVO7%Fh*7a>br>0Sr2^eo@2)q+TqpiA7-9`45a3eZ&+7_Ozsj!fb#! z1`{5%bz916aNfM^;e!KZia5DrH5z=SOTcc2o7bdgMP?9q7Ez~_ekXe_d+S$9V>W}u z&2UnuYVX0Dc82W8!QIWuxab5h8Qq{5h~-fbH{kVYj2x#NnVv80ITOvzJ;_zs+EHs% z?&J^gjO_bk%*^X3YZ_P^3^!#xE{nBu)fUAj%|-yr8*#En=A7wd%2mLTd|D9Xj4U8L zcTiM~Ypl3@*69=`G#M2yc5hc%F`@o2F9ug$Ag%pDuq_#l`XtB?=132u$2RT_%uoHwlW#4hc#9UD~4|f z4Aq}|#VHY9z7ik5={R<(j5M5!qojQJfpzte23P|uAX;VBl=IIeqsC>e`V`P?ixfl& zmvtrcA^iy*dt!D}W6uVnl9NfpSbfAS^b(0=BL9P)^s?U}9Qh%!D%Q?Vv@?f#CHD#C zgs*!_307Q4H8;ztn6FT$Ki^(88KFn#sySLNA2l~2nhNo!)o083-EAkkC8B910&9I7 z+p8!eTjWD~9kR9+Mk$asmH-6Q^i6g<%%=HJw}L4JBq)NoZ^qsG;tAHNgrRQ^j1nDUYfSl_V^*xP7TN zXO8I2=5CPEXZ}y@Gk1X)(cp@iBF&s!0YL~KyGDp%200r$C-~`}MjG73i*L z;{I3=+E%S9HM1$nrPGYAhQa0-DkqaNwLA_J8z^zDiJ2W3qUGF?MnWte?=d@rD%>d< z-GQPPp0f=uV5Jge|5(iEjN{$eH0<(bM^ir5+9<5Bd_ZE2h(i!YXI$Z~@Y%)^TE4`8 zse0>=m60b~ymICuXb4+SbzC!5gZsuf)h zVj`eaeyYEU_sh5oySypuBLyZn)f#qwlkiBU3T$jQjpalh?A0LnH6T1sv`xfcY`c4>k>fHuJ2HwyYi-kCf{slcMMe-a=|o!_JpKadFgL-=A=K zpA1l^2Eun<{Zz&AsCvEog*e}6x9va!LXBO%jyu(z*G2qys;|X>(fFreD3E%y{Hg6n zto$|u;GtK;jR?X?gc>3ZYrncS7w7wT=f{9Ou2c4GwHLXyl(8eG@;^u8y>_S`qg)bB zKsnAXSDikKqwCD?%rW=o;#)ZLmrO|QzbJYJc>PG&WQ*tJo2JKQP|BQ+#-?bjs#E)< z$0UNjT9|u1I)7zNv~Wp}(tfQKmWan_Yjfv%FEr#}$iH65PQ}8j2ouRXu?4f?QGQ#X zmC{uC?{iTn=JY$|hSNcH4l^dtICM~*&>rfl83!wB>y~_vTPSWhBQh5?x36J_gto?F zcQmG5sd#hG2(#)f)2Y)~}9E6?G<(#3IV+_;Tg-Gt4zn2tLv!@5u$p;t2y33Six86)S*E zm?V|j2zk7h%X%$yV!3n{)}4W>&gj&aowm`X8;b?dT5c|k{rG`DncSS=OxN>0pB(-}0mUtt{~Nrw{B-qas|8w3vqo1O=3$6{=zOc6RXCx*d=lE7nt2~Tr| z+Q;qqA^T`#pGv|rK;stmTp-aPDL_S?3m*yB;J%zQ=gVtxy6UdthVngC1-(RE=Po!~ zX^6Bq$1CO-LCW36cEunjXk<#LUl?5j_j$1XTR_#30W+o`%>6MH_oVw{D4Q29-!=SP ztYXst0-!FE?+=oJ-AP9?sAN7@+^b#p8T%~3f>X1rV2qRAgPy;H%%g( zXhhuDU59+;bdjBc7caWOWYXm6#+8y(&VW!(`WotSQLvtr-_vD44I#;yzukGl-BpCX zZue>FvVeWqkWr3vNgd4 zy^o4v*+R%*IPY=}vNS{jRhtXzx+uNC*M7OvTZYI(X00cwv1~FKFjUpCw#_iSdmTH0 zPHyGNJ_>)DHa@0vK7*77-eX}tyG48DtZw7F0{zA-bK6VopwMUGB`G~K6E9@EB{RC} zJFKHEH#hEy-Fe$@aD%~Wa4TlG%ekka%dV#KEsfI>o-8ZP!=($7v3Mt~30hCO7&EMr+by(V6&@tKF-NF;c!^3K`Ji+d zmV;57cCA#5YpKH(dZen1AF^*?KmP7x825T$G_uMB{$=y=bue-d7A&qrZ_4|mMrW&J zGKQfI@0BP>KaZ+lF#$U?J4v1JXH1UM22#Yqhdj5DnCF9Kq>!PnhoLI0ab?G|H@QOwlZuV5d%5L0TwnakA*bbM3o zRv`Nh3Z?KNis;Wbuo}nKbks=_yYM3Vhj&t~E;)$sw6V@q3K02X%&=5bKm0gas;R*= zp=8ZZ7kXls0XP-uwpAUD8PU4+2#@djNBp6KEr;W}EIj$Ti7FhfZ8HabF8)aRCF_D4 z%m$ZrJd@7@tZ*Pj@;2!XNDUzGs04jj4ceXxRSEFf3n#Idz0M0K+>G?K@6T9pg}KK1 zcsxofBv17BQ-i#={^$_66cbX_+z{A$WVfQyjz7clfU;J^t3`y0OFvSF^Gy?KM0s|5 z?>^0XE}tLy9~3;FgA=;up-b6)&+F?h^w`C-L(2V4lk!J1MtAcukp?i55NN(UyUGq1 zlM*pW77?Z1PJaXP1{?^<#M{RiV#BnED?>8CAf&{w=iwBZq<3i6h7zj*%plFmkNBgV zjSRWp2gxGF2%l|0t*Nfx<6nSl8Kkb|29oZ-{N;FbQl1R@Csd8;uTIk6P~PA3Xn$jB zf6t>a{Z;$=AA)Ruh3^#T{=ZQ^{ylZZ{154}@1dN(!#CFNzSX};ci8?9yHp(il*IkZ zCHl{i=)aRV#((TmHK0?+Ol1l`*}VH0++AU5wFK@Y-hs2oDX z39H+Gf1R4(5u+s`<(vIp(6Q%e|9L!-TCZaBc=>4EYQ0L=t=Va|e)yWZRWrEQ;im7S zt);`Zy^=>ABwsV$a>%?%^@s@Ekb)H_lRfMESo&&s@+I#8T^D(k8E`YF?nrg%bPu%xow} z!y9z(=+L1*tBLoW;%>e2UIRM!Uk*y<9LsK+G8+e^~t@7Ij9IsIM7q*#&o8xkP{t(ZLGJ5)o;?H zXE%cAThC;{F(@Ml^pEY7(1sO861HYw{f+HMIQERYz1au>XQ;;YG7#$wOT~rN1Qehq zA0Gxcb!fM|0M0Zsb5EV-nsbll1)-}B3O6u;J(FQZd9vn|>S3E*LKiV_g} zX;!(()poUqJ)Y&cAc;@*oRo7LCK)|b5A&qd1QP6i zCmJJx_JW93H=nA40at50E>nfEnvXi=Aa)5!PyQz&fpSok#OP;x|0Y!H`(?lPN z9vJ9f8#?4Hb-$HI^DjdIJ-|tNU=qqC0c#>805}{2?!T@dKWq?s2| z)RBTt;LZ`R8l)HP>sOb-k>mj!Ti0v)(ePij$)NZ&^E#F)KHWjWw0QcUm--6zAevET zjRAP#;T-jQzW}_<)R8QL@J!zIAYEJyd}bXyL7@F+p6>vJdcU4k9aNF34*52wo}WNg z1Cje3>wZYN=)XaK{K+br3{;P&<_k@N=-A0Z)>&F|#5qvBnro=ALYWu)2{ zNyNQIT@VI)`>jB1_^X8;sNWG<#k%vW=gN7)^oyeAVB@#QNtC|zP|J-IP~0*1N$Xh= zZ*Ob|rSO?i(LlJudp1V4?`h0WLRMK>*XZjPqjgmDJnFiwTgJXjLS~nK_Gt`ovulXx zC_j`eBpBYVZI%*(=XLQ=g+3HuSj-DnOQ1CKl?p3TEldg>k>WbJBdGGRd@$>zix(y<0JP9dMd@1FeET905i$WnO%v50{6L3 zI4PVryvnkCqew&!4deqtIgoM>tRg`MITSuyI=2@WVA*D2X7HdRXjj9p zRa?vLPB`H!&xA*XBkh!5_aJ5u26OaBti1%f?8-({REoZtCbjPGRe}o@%X{;8G&~Pc z>sthsJrc<>-I4`W9~`f^>+!rBK&Q?^;$b(mk8>@0gUb}8uAya*`O9%eadr$-D?4H@mb z^B<;qop=ISmZxgtK>=+5X#CpN;g&x;ojpuz{d76R*5T~p7gE^0q)pJF6tgd?fCq45 zI?`;T?05bI$7Jk@oML%FN37(#6RxEV#7lttiZBYy>O1Xiv+xXjSnF+sXYCr>o#z|l zHm*7NmhQMt_Z2vTnU(>`|anxnC9rG|?HAimZ;c-hq} z6H0@l9Tdb3_f>WdF6PbX1%KtU8P(DNCX+Z>N;cBbS;?#_#NVpW6P6V)rYYU=fVa%e z+h-a3(R@0ZN9Qf*tbAD-8)!u%Lm;#7*6EK_MDgYi<#zCEHCs2eiTLQv?5e*5@NL_o zV@unZH#TH7wNtiC;SL;UR*#V>A#BtrM>#FVhk4iDoW%yn0|mIcX$;P=ff`Q1A!UCfZ}& zLIVE}U#9Gj?a#3m2{PiUj5u~PFiaeonNFqrd{G;Jb6@j>*_#ZrLZK`$UhI?bz&jA` zDz90*+I+XRs@U=D(tv%_rlGpgnW05hU3`RrwQBYYF?ko1l1FW*F1NnwbEilmy3+~` zx@CBVaI<35PiFIv$7@dxX8mzn9*~h+-0@(6F%46k;_?<0QpY2AHjuA^!^#dzx}N7l zxzAs&ogXC80Mr^*vv3=t?Gu5cU}qe@-Th(Zo(ivXd40FULZx`tO-qpivdn8|OBYfq zfaTkbaP3m6T+j7t##obHkw5X*nRrMsH~=8`14dS!T?5)6$mC;w>5fSmaWy%p8%n_1 zMNK}=WwweX$yZ|~IG=4nbSt|YET6H=(&OTJ-c4>R0Lfx|CBwBiO2`t#-Y)>Xldsc- z>cW4{Jn-=Zpf%p$6gZdXM~5YD+JQxxm@$aWAn5wThJ_%=W>`07#KbVu0XCHzNueuZt=-M{_=0cFa)bK0~bHd{Np`jzjE8C8^y^~WoA~^90 zJ-8re%1zi^_SDJ&n|a1XdF*qD(egDaM}>ZI?{ccjov)qL}B-L9TG2 zXi8%%G^SDBEkJnvX;}0qC5v)UmzCB{h~RJP(e1N~mhc6~3+s@b^iiX0-fFUnNpekHjvP%Q&RPglzlP*>VC%VeIgAk-PIG6~D5Gcqlp|?}IfpbJ!c5{& z8sBbbn}sXyAV+4r1SV4I8xv?I^N*-#8F1?rF5Clu|Fmv@F>w_`wQXpU`@t>a@12t8 zENM+MxZvr6$ljDzP?7`Ugn$D(Q`>f50gnVN8OuQ;U`9P8$c!D~&Gn)FPuw zRc8jVKhRkLVO1bjtn8o3s4vRdpH*w?vt5@46*1O^Vm}#ap9WC*--ZMa>6ItO`p}!0 zYM~4y2!N@aRp>rC8x0etvx?xA zfVfyS=5cUrtAaboYrm5SVH9A)v=e%}M$2MLG+38sxM+ou9CWRNolfk})5JQd+b0tD zbl9p2;FwtXFSesYgV5QF+y;d+GPZb~+@uqQ)=UhvW#O9OOLa`J8$!jJ0{eY8i}*@? z@$$O~($>Qi9JQ11%CF&UPW!4>`=ePAu+RsEPk~ad;oDYy+9aF!XbqVbmnqalM}|1> z3!BcE7=3_!v7#sZ$5$MF1grA_)|QmnCt(a&1V~zlY<(&l z)Rb0*!TAuGHJoiSxdh!Brg6J@Sv#NKf28cNBg@ew7T=|QkV)M8L6gsU?_*-~)w?~ zFM3tGvja_mW4t2?u0Zsj?N?b$cl`~Fz-`hYwfl6O%OfV7{czm)yBcL?WCNAud^<}e z87$#B%pD9O8t1hHlNf7yHWZ)VJo1b=8lUOJPa8X@_;GooDIuwgrv5?(`O%(^jVl~J zM5A61fAD??|3Kaz0HC%T$JeO6t~adF4g^YZm%2NplYDa{8hlXW%i4ZGywPf z(z(2jtlTdB(A=Zf`IP$ejxa{XU+es45v6lruO58XCUK{81dbe2g$hyT3;MLe5Fojt*+#=K?oac zMn=f=Sbf75gy|>ofScbD| zh~4M2+1TjF6|<0-MNdB^=uTKpb+iRQ3JFrMl_RFk&riZkrZ|b z=Ph5`^DZzyaPRi;6db>6XCyp0d_ob&k)t;+sR%py=@%#lK)}y0-+u<;{=sVWPoiA^ zTc+;6Q!CXd{cENU?5|d;2&vo}ibOiUnFF~MV4eXW5DWmB8&u;zJBfcV0{t5eWcZt9 z``>BczaqTy2*-_g}$0^%gU2A*~vIUv~5hYE~wZxs>E7{Frv5c9%7Ue461KptmaxWfY1q zFFVE+0QqqwW#M0$oK9zZlT{kqO5;(T?oKyF=Vgalo%#_GZtX@snwr{8+pBrxf%4SL zBBuQnGC|6zMy-=U3r2wBJiOX2)9Yzk@A*}~9-_1+POHn$oHvy=+pJd3my@ry>gvZ| zRtMLo<7n_w3@`LnGqfg-<0x3WIpPJzeorP_2D=1LW~)++!JtXFC`u$1bEI!)(>z~= z9e(pbt-KdOg`@bL!Nuw~26O;rzQrOp|ATPBV3IHmGHZ z+)6u1G%i_V3KP8e)V)R8RI{#*2q`Q*dt?o4nYhc;g%;UTB}38Z?{J=!r4nd*>s@;3 z!g_}}!4KIm281M_zBoHSf?NcncUJlDk3Bk*&O_~fMR}PD8eLeyDG+aE)^qnPOORA1 zFqE)488V-=kB3axv+g_Ir=G4rf$fY?<>fBt;bloE>9XUo>I! z`RVb6sOw+{Qjl!5-1vkj6N&ocv$qMKk0juGAtuQn!P|8G4@tt*@mi)+I6v^IlKMVr zzgj;*0wn|PTZnB9FTynLt3>}(HoQ4e>xZ4z90X+=|+64e%vXra^4KDc-c}EXT z>km$1h-j`PZ)%9B7(ed7w*eFOOFl_Cmvm5I(bu%CPvZIcG1)~oPm~6Oo;!m$FBU=A zg)#srsDP;G^Sm#06mlKgUq2}*8l~!#jsJnYMPJ+Vlm-f!B$Vild#sp~^z&%lN_9*W|oNr*`NZm>|7s z6E63yWdOciv<&VwM%i_fLvw#yMAS_Amu_Jz`7}^Ukx5LCYS8d73&&N;TW91ta7OJ4 zQ*quD3!@5KIm?3iVhxLu)_iYa7YW2tm6bUyA>QisM{YvUjy-bmg--J28AkyPbdG0v z_ZU%$lw#5$fD`jGl)L)P8Ue$DJANZ8P$_1CfDpJ`qW4Xabx{iLq0qtM!1n}D6~Bb1 zB+8LIzZ?uWF@lnB4=XVfijDOtriG1fo5)p29UBiku_l_Qq?Rfm%Q8g2RtSK56hU6D z;0rB0kZ?$Y%p|}1M;SP4AqDv%vtdS*yr0U(6VWAt>~f8TSbN{6uIn;hn9OTXy!i;k z6DC53zu*=bB{-FrsGssO%cTZI?BV>*2brDTx?k$!28YHjD$TCQ3>J5%Q*GMbs7ZD) za0XngN|HQNVVOudtYC4&v{eX}%iG2rYm>F0tHRe&#;%#g8ucHb^?ML7xc=<3baxfD zan2p~C-$QmN+HVon&&$-sPSs5zP7vg$1y~;^{mUMa@So>CNv7i$GJoca1#&X?)?aJ z;9|e~Df$T4Ea0CC`(5~=RYfBTbNO_L3^ibF9DJYzL>pN}Krtn}BcNuqb6-s8)UJw~ zidN08ovpdHIQqdT?v>uLnAp#upw>G!(Ud~1z`R?HMEK)W`OOvl@AO9foVyBXT2EmQ;s2UouU-B@1S;ziXjn= zF6ng5&qkIjs+^=JEt#wV1;v-fLH^z|*v|$BkN>9tPAH~otwnM1$gb)Ep!udh4Qt<` zdBuh8@tUAwI?pO8d?p5S2_R&E90kO)J6((Vh&>P-+~f^Lffe6<+8TpU2>~D7d5CvM zsdES=6{?%5`pi|06%5P4+DK0p^dI#$l||Qs_25CTuHJK(IY2Pf04y+^@??}~y`g#; zI?d+j$+ENkA8Xn_BFPd3TlQVP=%1|~l^U^0Qm9^l(4eqtYJAWdEFX;!aXw^kagp#) z`KtYgy{hpTEx8QHc@6Gi^4wz&v!Xm>55BrCYFi2syu|>7u4<%?#Y%Oy15`gzu{-l4 z%_x*os)Vwab7J?Rh(u?1Br;7oUR5RYWBR1x@&1_Ezbl;1*Qu??db5ct)l;WT>QWL# zSRsiYNy#vxPXeaW6;I5j-Ik-j14CuFB*bu_%`EH0c6hhvjR|)`SrT^}l#Z4(Fvb=E zo+(IBgFcJrPGF}&rQ${Vu!l2}_332Dp|H|;AcIeYxKMKIb#bL?0SnaGf(phspd#-h zL`Sl}!+shp+(V46pksswa z+S2p!fFK$T9scE7`0c_evruc}V?bjobF~ykl_G43YAir9VkqUp<8>4yiiM8JZu;S@ z@{)TFFc`#aYowFIZY4OBmIRtMw4&s=SBAG!mgOH3_5PH_^oZqM(EBXtQQ;v7P3>8) z=JGN77A!M{{7ScD47`ax5I=wWJ7M^P+kcQd7u=dWA3U9BGL>iw#=4gIltM;B{@QQQ z3gtt-|3~ZvM}(`puInYHcDp9ssM?}XS2+7Du-u4L^^eTkinJCs3;7kVj6nsfh0N|= zZwmT!CoW*XquUJOPUz4I-tvGF+%P}j48)m!W#=ZaC zfVQv&8@D}i_7wsbs>sr#KBw!tMzW(Aa%F;G=cn7+)5joD&kG=lz2hRmAA5r)<|$_n zZDRzfh2%xW3~ogP&)}o-=xqZl09g>QW>yvy#1&uatMJG+XVk#sbQdVv+8+@?8=cmg zvFAZ=IHPbcd2kHpOATsW)>sWQa&Bk9jaD5Bfx(5Rq-}-)_WVibxUvkvM>}n)uew)X zv99Xv4k~1F?Rhz9TX;B(5;Dy8Ia4yY&?VXrDncvq+2ceajV7uMjJQf8Ez-)X2E{sL z*)TI~X=G8pvcZspO z;csKT$F&9bay$ftUN29%y|}}VDIime6i^0hUE$~+AS2B`Mcotco2-O(J$yi;4e$xs zp(z+?+{NOH0N2>TH-GoVNRgmj#FSCzig|l&d(^|Di=Xx~Q^1+HjWfM|SWnzCh^ELlPoSNDFZQ&DfruQm(JxNVrf1C1h; zfEg(V%n9o7Ezt}hflBo&q(a;6${WYZ(1ZF#GgU6Rw21Z_Z=!&;@G&8Zea>cFQ|kQU zBr}m<7Vj%(<}58BbDHZt7Fou5kU@L|IA_2`2(ugH<=h@Z@Fxg1Ny|RsG!`Xp1Pyt( z|1Oe2k8!~*288O%)MSIt#mBr3#udhZy=xRQz>n=&ihH*BN$243;dJxSqm#4ekn7pS zWX4e%Wi~>TofD}g5Cie1Ytp2Lo|zoD@Y%^@wrnuUlM2}aN}?k`_0O>$>9(}U?_rpb z2SE>Ve!Qm6@l}(f$4C&q`&ohmQVC}S=vlGKrCnnzkR~0)GswGM{YK&QBhED`QI@+h z$)>=|zH0mXE^Tw;PTbh{YQbUr zcDL^P(cda01PFzNN)ihg<#2?(i#`H1ft%$o31v0z2F@7uK#@I=N!SKNJ$r*|fEud4 z?ox+#ErwYsDr6Qil0x4B8FIWYnJIE*lFNJa5lcKoF$g%V0n8vLO6!Kmcw9V;1#Tjzn)$?w*wx zg%sNj3%KsUs>N8ZZrF666z1W2aW^;P_{tg#g(u1|!V%g3^5m}pxc3*3oGtW{bMFTh zC$|l!Yfw#G^dSNV3xF=@D!vTejivuO)5z3IrQ`1!Hg*zn5c*_U-(I+wxylXP;Vs0J zdk@A}>#d1rhj%E-w{ajXDn(>cR&f;FoFDEIv4VzoW z2{oJQG1N1W`!IIIG0TO;f#yATng_jq`8jpFgay!ER@%mhjtZ{;K#BO!*z;qLH1??| zYXknubc1T4x&x5Mr5^s|uH>*GcBNohUrY5sdeaaQBV7%xm)SOy#G~DxNpSb>2P?pN z_m?dZ)FUEgmp-0Nt>Kn=govDio*#5MUXlI9?NC9+^tHmFgw~#@6nK^eh&iKBhBk8B zL*R#G!J;QOBHT;b9?OiXLsN;=p(8O4wVcg5F*zl=Q3eRx6HY|T_xALrnYq~$1o~1h z(!yC&h-n$&nnMOGx&i4TOmQjZ7)oM|!*A*xoiTf3KYMFGq!J`PKd)_UCnK+spn0@wFpyQKL3Hk$PcMGgM+x;{z*(LsFOh0%aYVt0|K^gc2`gHFo z(8jkk5;!u)YE-}oVC0KE zVPf*FOUd%zGs`0r==2fIhm|~5E>gbfHyE>U(oSM6Z~AiE`u?30Wb*wOe21x9WbAk9bms7e6bz96 zLYVY~Y}msgkd%e3z53Z)Mk*)|LU_}tS5uf#g@*221or22n$7EN(x~CS>z>WdCR003 z94`Lm$B&4S!{j{GrEYVJl6!DI&%*?Z39mp%$+u4U{9*E#t3Xm;F0gh^0Tft<%`jYY zF04p>+8HocF+8=M+SdbQvjG6tju!H<(d(Cd#=+^p>JDblamp{hlgN=l`kYu>&8+1} z(LOu~P|u~pvDjnH36MctUkV01WDHA#I`z@87+;xP)DuRn!VhVrm@>I`thBCpQ0=bAMo){B;*|Jo960{Dw)4kaP1fe>S(KqKe^=mLUK{%7*N%^Ir zM^+ObPIy6ZnoZY>vFn4oj@5F6(V%u$hr5IR1%iex0( zRE-dp9#ax&X$+@B&FS4ull#Wu#aXjw?by?Wxri)V#AK7I(t4e4wYFBbS3l`;{pe!) zH^_wlE*Yz$#h0&2F%BA^hoLB_vue~)M8TwU*GL(nu}bic5KHbSOnOTly=Tk!-H{)? ziQNe%+*l>t)`kAXF-L3%^Q!B+Wr^3~O~ZX=*F@@lzI#N&rjAW%w!z2>Cj$%LYl312 zx*-W|FN~2#oOF{f-#3|OeLQIvoh{g@#lA9E(+a2gsNZIe0+cKb^No6ejK;c3EUbzf zHfs*Y??oUbxP%4(OyDVMvAOV@@Bb4&vMNk;MqBJCa%R~MKmVZFsDaBwm ze2}FLw0Za*pQ#x&6Zm!(x?jE(Iot}t=**gZw7tH#@3@h3@Na>5HG_H%S$&Ryme&@h z>A3v_ymmE619Y$mBwD|E#|f7#%K|0nNOkadGD$<72xF~5VAzkJ^??JYEHE*SzcY=& z^Gt%^jq1C+;7^=s_W|HBXjbttN+j%Pca6MyYuK|HIldFpV3htMZMl>|rpp5W+x(CS zfW_(%zFG-DBe_1B$>pL_e(*NlKI2k3R4McbcmLE?9mDG-f^g+-wx9MMcq3_XWA~> znz3!$w(V4GI~CiuZQHi(RBYQxy-_8p7+q`aeO7;``#XE@(Pxhy-M`;4o@czj?)l97 znsZjB?4345uo<4639m`DeRoe3ow7v zdTq*X;D3IR@J}|!b4}7R?;TAaaJ{$Mx6UCu^I4G-xqKW+L^6GtbPhO-X`yDiBxgV& z#Aum!iuyBp?q}n0SS|_ngchSuY1%dGH@WsAz?kLRBT5(RFp7g`2*z!pj^%b{JyXFML%!k z(tPq-Q15D~_;u*;D3fU`8EFfE%cUhkMrrc`v9 z6K8f?zj!A{`%<8h1<tP2)8mdB}nTko=cm&&!1#*w7?QN^p=1p}dz!w*L!y>Glsi+}L*CnCGPrZ7f> zlEJE5p(w#-XThZRo81*6Oo;Vs)255H|-6B=Sy?qkZpN6BWkhN?i3fR_EPCiH+K{aok z2lK!84bh6t`Y0&!9g4eX8Yy>Zvy&r|-+IWHIiy=w9Z+AhDV=If<)XSsHPPAV=E6e< z3WDA4>vS@B$eBhp^^#T?#yB`QWWf>;6GcTEZ9=F1<_nH4ni%4(|K+19O#^@8=zJC( z0!fanUB2VaZ_@$F8+z2lAUCequ?`Al;#mVg{Am^mh;d0xS(~Dxc{tT1$leqKuU!NB zGkj(pg+=`|Daj6oG-m;$=SwfU-1&1QIw8rHPQmgZ^0up`k6`*mG;aQiS^ovR?a;c;y>5Mbt+^T#*y}{5^C@c5CvgpabCT0s)zxCuq^%|$IT?b6=JqKUAt-bJ z+!}~qELeYl@SPxPF3!pX1MZBfS@DU7m z4Bt%;A(|ED^B$n_Lt+(Bwq;t<*HdC+%~)PoX(~=?jgR~-bj5_{p_AjRz&{~^R!UTK zhudhsdom+#;@*ftGm-2fIm}#&+#Y}=9Mo7AP|=_kDiYO-CGhFzpa`d1J(n4Gv^Nr0 z?NRV2OePx{hH`v#Z6E!|u4oHobZ2YD@tuL@3XfeZ1Ti4aZc(7v^B;h##Q!-5yhfzT zOpUj^HLetHm;#_}r^NY$z$RaHAyglcT0EB1pW6$iS?%|7J;87ff%#ca$0!mBOQyHB z^*9pl;ekW#BPxGr*zO}r-Llb<$^*ZZ_$q@(5KIM7f?S~#jv&>TIH{B3A9mJAqmn6 zwQP@r=mls@95YWqIR~1p`*j}~DJ9!Brpf@B!)DvgesdO3g*yk|sklsHpj?mL1{U`O zDp^pRVSK(Vv=4zl9GOvC6`5&2>3?`)3Y$Q8AJ=BmLF8^fj$>acAfsc?@1yG$sPT$Y zUm))Ai~F!GT3g?uYM(`wtwde{=cEU|Le^?|0bmWIm-0! z;i3OnRR8_<(m((Ge+me&u(5LgH7}I%4^iFm@1i=wA`y)mT9YoJMXv>t3aMqtQ~GJJ zEoG!T4vovn`}JmSI$?RH$-Q0ZyLeI9u}?a7oqhbyr(-ip;fniJI zas2hV?t{?#hjStMR0_s7)?#I&5_US)OexjG;fDHh+o$_hGeG?BvZ{C+y+wnk>~i1c zi$~jGzqhOF58k#eS`C1^0cxAK3ABlQw^=L0REV^?KpjdVSz0PD#!;@>38H$$(9poh z4jS~-VoWMyHRR{sk6WH^FU-pC^fb=`lO9`Q%8nG`C7q@VgeU&$&|@-8XkTS1v6F%t z5+&3S^wob_s)5W^*bwZ<;^abDHsL+@wb06QeB+EKILE2feV+JWYeQ!r_zoLTy)l#z zcu=X$9hbSnFqG$lCk4z-Pg_~wnQwG{jKxb?OUWQuHtHIFqY_UO6kl`-v%j_{C4c!y z-`LKaSC1-GNq_W|p?nQvf;`yVoZku}eX6zx6uyKoQ;ge97^QjFHC z_m-Rg_+pc)UA+o|Re%ajX!PaNroG4{$h~79wB%c;kJnF`lRVnv4q22$gXt1vieWah zv4c;BD0X{;z&z+L>`Jix)q*fAEQkWi_fFB-Ci_XLvLv$pz-lnp{b%nIw>MC-IqujpA!fRGiL&Tr^K0>dQIu>ta%<&b_(h>i%7{IFZqIDe zUEVzgWbPfERC)?Wt$0{=X&DL$WNYpw5!B$*qoP0!`>_1LyndtmkpP}qfOg^BH5_XO zp7BXkY59Cm+J&bddfVZRJ{d|v!7yVnsF*5qbu)n6aB}3S-z=?9=m9By)fp$mwE|K1 zO!?Wwzs+Xj@rE)1mrO%mu7hS9_j4_0fj-VFn(ZU$kOT`y#F*`t=e@ZS3t_mN>$2ru zRDK*vX&5}XbVj|C-!K4wwW0skdfd1DHOW`=LZEf{#Z;6n_EWqd8Tvw#zb59mLksSuny7h_ zL*$bqG5p3b@eI@u?8)G31eqZHb7c&s&q2SUX#Gmjt{#z25+@ZV-LpY;5jEwgT#s}Q zgXrj`9J@xrl|F)CMJbeDt!=LRj+)`&uZW1kAns;SnF~b`sTCl3=#27%=6KG%VC`EW?m>V2YVTA--Fb3<(d3~$G< z)zEi(hZB88o=&L0Id!mPebCrB)muthtkCkic&xaxzb zDdq3wn)~wXE{Pun<6B;UrJ?h_()9wx^{@{j4gP(PHqv(c`KV~?G*=fn?B^-q=YpIS zm*2WNk@KPB*&3qPY52V`W5`(RD1nth{IOCzD-t)4@Zgq&2g3{d;8Qn)0rwkfv6L8F zq9X9=uA6bsf>^pRl3|xD>~BOwYthHd8b3^;*u=OH1--O%5$x+j=MKaUc{_D%4l5X* zG%tIM44c|_!%z7`>lf<-+xI|it}LnU3>C*7HxXS)JYuZrZZBqv@N+q_Sk~8mxDZM_ zB#yC$r!fi7>Zm6*ds=&~G}KohhkHP;##CX_Ajfv^ugIcd=P8|{0+1BD1B6VtKKV$X z*y{?1cY_Z0#vi|*f5(_w$5LTPuEyiU#%G8AAihxz&ZF{;O(&e?UQ=fW#!Uvi2{d^6 z?&gKDu+WfiImQEtz+4$$M1`jIYPH6&$@7poP){~sXrERPXcI5x*)sO^(77nhs>`mLQH{0HaMVH>-cbk$NQgq^ib<4pvQI>h7 zIZ@b%<=%`wtAZ~nN8LH7Tnu>41O@~P3LlIs98DhgLlo&!3uW)pswT9NSWd~BW+IH( z2%XHw=s`A&2gtbUO;@7#>Ca?#EA~5lqm6<2CVoV|61)0@hu~LIKbThutELDxMqh!< z@ligbr&y5D@wD;^s5z4wh(=L(nY5NapstivNNn^l2_aR!BVVG(*mFC!)MJY0e7TqP+lZ2{*zZf zq=7uO2V%k2tJ7vH&Cfu|{0dd17h0a5rsMwgIV{cOHU=uyvyOne>GRg{<$;?etU)kdKH$- zhUm$zN-RKgO}{;pfSF;by3_{RuGFi8SC$Nqph6b^m4`i78TX_~2W25=x4eWVH>eFnIOrU{M#7 zA#Xzj*%HA5txl~Z(J63a-Awy#o8nVNqaQo`Crx zSZMI;kR(wpE!w38I^Quf8Yr+OeNI{#VpwgcK@-MT*3gOH>Ol9)HyD(xim*&vX3u8Gnz%GpVH-6xc^E}SYpTWW1|-7iO-<;l5)9BdW$7;@Eo(TJ78W^PSjj%|k ztdO)xkgJD`RZ*ckV$trEK+C@R=&Q|z>rVSmG|Ty+-Gle;m?3tPRJ(mA_Iv;JdK%o( zrLQ|9=<_)GO6Ef>4>+{y(|i_*9{otI_Yp*iZp}DIMvty1QL_X?_4SkTMTTtDl=7hiUq%beK$Jg!y|1Qg4_p`pAF zX^=U;N@)Cv^csxaXP=df&9v=85{9|Z`B2PC@Du@|^rjh}Dvl(IT@A?{E zQOFPAxrhCyqwRyov=TNsASkI8E68)j^82i*rf+0|D=`a6@gW5bRq>k)Knxp)qLg z6w5=;es>U`R#I>|{(u<}Ex*vq1G*;VRH6M0jlKhELVnxt!tvG_rC{@-*9%l1$o+@F zZqMtN<;?iRxE^q}OVwM)(1aXO&);6eWJ;3;6T^!?J4__13O&TCf{l%omO-BqS(V zrB*h2#WM>S9wSC*%P%l+psm;kFF_34>eCNjM;D_Q(=n}DK zsOPX_FD$~R$wK&9-GIQ!Z`45w3!XFFD-_?(U0MNoyY0~|b*hlj$163%4q-TE%RXm0 zW*$<`M`#3f3p%zZ1ke+*TL1zAH{27o0^JSZXiKUAkujF+L=4Uxc=C~=5MpZ>Yb*pQ zK7*sXJPg|Kzd)fD9mDd4pdmWo$#esUfm@S zIQLu%GtE)Yihp*Mjs=Ldf&S%YygPhqq8_TNuZy}c>5d(^Ga7TpJ=8Joq2EZ*q z>GRiIwUbVqr&eKPWH-PpGVtJwSZoU2HGmM>dsWwEx5bX6T7qjbS6V+(+G9Hv@H$3D z3WEiOz_DOsrOJ-SDIcM9&F$F5OV0k({NCJ=Q9Dj zyT+E$9qAs76d;Oks3IRsy2`h&`&}=!$2y3|+@%4eZH3V~w8nC2%#NPbZALmP@rGtL zY__R&O)jztVCszy^o-0*t-rw!@)P#Iw+&Z)0@>kcq&Y&c1dP#^Et(bSxFw`)=GTlG znX0Oq(I+c*RA{P6p2icL%6!G_fNzGx(VSY z);rg+%UMr@xeeI3HeyrcW=|$`>Yhc_#zj#J07Fu~{>OJpcwjghw-Rm1OxMR_xM7I#6TQ0htLs8{+U0$<~>$mK#K(2^#|Q>)4cAk5|;6~6t^GkAh{7j95DTljiHB8QTCO)=y=u7(Da)TQxbsn6#Z31$&$ z8*Tn;V=uRLg({pQ()Rj1qw2T|%ssHKfhk?ZEmQ}hYl}m$QG5|3l)>^;L+#kR){k!s zY=l=yfInX^GvJ&fuR+kGk~QsAj18i85U0xDKROVprbtBP-FUtC)ZDczDNXYc$}BJ; zFvY&RPd}rcma|zSwcwm#+%nPiHjXVbpTJ_>yZj&_x`mR&okgR=%!EIjh!iaWWJ;eT zfpV7q&Y`Xys2Wz;Guvb7Z*A&wcf2fK>>!&7Fok)}ljE4cR^g+A1I)0A$@=$^HX1&TJiIj_u^Q6Im*_LXAse^(r})>j4bZ zk~R9f(jI@anX7t%Tp~WdD6d&hj~D^0by%aE8j};|PXI`N?UzhvP%e=J_L>AjC%D3u>#n%3yKFS$2g;jjN=)CGp5 zd}0vnj66x$myUgL(%O4-t(@AO}DRD}ls~DL(KCkX)RZ|yx zPw~u~c-;dLI0(5M8R*kXlPTfet}2}PrBUb$Jf@lufslj!kEj8~@OV{nfFfg>YAFe{ z`H0^sPrUpx{IQ_hrj6|g@kX+@223-%UzAA(;Y40|e-v%h5z zNW1(;23&6>#8NW~4}NVUWac3672(iEL3c9242qaG1z1NJ@4F5aQm%=RN+t~7a6cNB z&#iN`mt8{2ia&u0C$Wl&(r&`go2N8Xm{&+Fm%>kbgx==kRqhinNI4QO1l&$dX)wu& z&=PTBPOfh`vnw1IJ>~MJLiR^+mQN#@~`oIqDI}EBvU0;%0vXu5dJ$Dr9y2{h61M zlen+k4}^6o2P%=0usZ})ws8dwx2FO~SX+sBIh{Tege)%2uOM!Oy4=Ic4cMoxc$1wg z%CO>M$G#r#<9o@nJ@@oZb>W0`@5n{I3HZR?W`J=QMs3IZOd-9#S;uJK{Nh z?dgFgC$pH0A4_`q0pD8tqqPG@tY3Jm42$GB*Wp{nrC&-x)_ozz7bYttI0Qj4=S9Sj z3BYuWe6v*0>*HJI`t@6DD${_v!iT_W_Xek#8e^r}L)D~~8f})ml$8V`{1c`v{Kb4l zjxf!OwYif-W-$zsaxouEgoL(_L%Bt_R8K2e%~Q)y(Ibw=d0r;1C7iM>SQO#P_E~8V9w*`rU7$#Tev78w zNb1>eGHix6plliq?C(VR7H~@G_&>N8l4a2pOT+*z6pWEDHKu}*avY}Q@HN_5Era4v zM%DOT)!a|0*Flv(2z4#|Jz8%!`^mB0?MLwFrIV#NBTGyLPKTQ)7Xf*2dm4O2u4!oT zHgc4snnq|{*MURpxv^(aBAHi%aZp_${Hv0KOhVL+j=36lqx^bSvoOASGa~O9S-+U8 z{BqZ8fUQesQ&~7$wm4Nm+%+QQhBZMOUrfYhS9)aK!_34PszcvARu;!=(Q71!eFw^? zGz}zJ(1Ov&$XwORFpX+T>|8N_ou$;~bO8|(lu7)F<^(Xz{-RWe>r228m1z^?YV!P< zs0~pxY@M5{MBbr<5B`-~xnw9jYaU||U5V{o^a5a&xc~Y-wmxoB#EC^bb4{sZ7rJ@0^prM0Ee+ocse?{BNo{{I^H_ z?^GTBKMObiCD8mY>*PPaqx}02GuwX*aQ>fe3G-j?C~Guq?YFp4KJyHOA4G-mjkD~F zO7wAbm)sn^*AIhM&(m!j9q_mx2NU78q?2{F{rfWusbrdJO1$BbqA)74XSY}p$8R{g zPkeZ`8k?Jcb??~m=p1uiTJce@U2Z)e7WtTJ{A*yun%4NlIBg^{fk(z@EBy_yv&3V5 z)PJ^=?K3=v`s&KvtB0|)(?qxa@-iC!ba`y-#NziQ-n5Y>J=WHIXv&uM6Gi;jUaQ-}@g8Mk14gL4ojlhtW`*36KnB$|ZD?HVp3`Wug(>$NS}a<=T~Xp` zv$$jnG6l0m)>GI=RP@w<%l)Ie0r4!#w#h z$+$h~3DDJc{F+*DGe%Vn~wmr#4qF4!dJPk%Yx{X#97d6r7jPwCe4T$@lq zg@yNbpmR_h8;BJ#(_)qJr=Hqz*5b*uu(#%ggbz`%j18}`t<r5%tHzIaul&&sY0N zd$ngh?MPlK74OA@;TU8mYfSj`wD9BB3I2lmMWuv1WJ`!WhG>}RJc-dQqxsSupSG-2 zR>4SB5x?ULMWy>4f&5rz!a+$Sit=+xrT=103R{2P1)>`)80?T1?HlM@45a2^%W8Q8 zMWOyX&SY7>@C2y59oToG0BIb~djtUt_2N*Pi^>Hhc7SoPyzzr49Wzh}lA-Qod3(a0 zXDE2MS|u@V@MFw;Au!e_R|hBjdBPz`Nkg>gMyg@PiF3V?YIOxW74cG+1bU}LnV!X+ zV;!qqh)^}vB#m0FaiP_K&FfWBkLQrp^TX#FVgR1nY1|&bKQt#PBPm6lO~v6=%uJwl z7D!~#xqb!gxz&*t}+hv_@>NrRTgZPXVZc{4N&+CNGf5Qr`bo>RIr%lZaa$w3ZLP# z2Q`DXL+YnM4A48m2y>fmfD)t0-$&$NPr)@`=lwONk(#nWqT5_@Qro$ELD8doP!5{6 zd0h-nX0JqnwScW-Z2(+dxDrGP?N^h>tTarXEFL zRX=ZiJjB>|jW{w`3Y0I*2|I-)-!FY&ntr9&x<79xgdMJWyYx~|Y?N4u;^wXA?AlVn zsdznA0r)}Tuf=DpV~-v??vwU=>ii2fxXZI<>gap*qq~Q;Zp)8hf6ZxG%nK3%6^d)? zGtr1}Od(H0&6h>k5=1_7aeaU0$T!JcglhoLCKZBWGhr;Da&KrNVjJbJp|Yh4s$?fhRy(vCGb`n1g3|W;Kr#A92@~A37`ew#LG5pd64KV zEz(=N%TjOeQUbt1OdXr6h!#PK6;J5+<3wFOI^G-=!635KLLae(k#%bpM$6nBi^5`AHlrCR%%L#2$4*6-bL5JMZWvU|+(wl{ zX{9G!V}+&biI9OFs)`rliocrkNEES-m_Py%d%;Yn&71Rw7mk0PwEVtap#d+5xLg|qPF(^4v9IvA>Bbjx*W zpJexILHmDwS466H+b}?EaDWxn2o(CO(AwuCwm>I7ELC6 zM|~Uc?XWX+QkHnL6XJw@*P?Xl@B=#~@+HoD{WvgYsjuVB{;3s0_vDVe8N)|{kd9n~ zE5=JhX+ffyJ@t+9;RRaMy{MjT9697~ zxLIu@c2Vao`mP=LNqItmR1-3N9gfRqC>lvztAijwoBZjp1IKW@tDK)>Yjt3Uq^+rp zuP^mQzU@|!1DxOKOIc*J|5Iv7;pY68{$jDrCnQ$CO~vugfK<*Hp3S9(ut4ia_mw&<*wWR9pM3NpVsRxS$Qv!2`iotho^lg(8~9?5A84f^@28 z_x1MM5~M_!uJ!WW0`|!-t?LB{Isy`*W|66cwR&rfzA}x7~=pk(0>n8B*|0*MZKxbrxfpd-@cl0H#vUyS_q>mb$}l;#}jjy$YYs9nTU*Kn&Q z*$aNAJ^^PdVLqsdB4nBz649Kk;$F?%^Pc*R=eYR+f*K&?HkhpznzCj?xWU~3z;)1= ziG(rzGNF|)*4!wvQPrG-Vr(GU;(jiB`)_7->r9f;mi#WV4D%`sSJ!%wNx3_rxdDlA zs5<$xLEVuC8^@w$K|GMDEZ|G?%aJe^X_)tdOgE=*r1ro@cvjgRu-g+r?(Jh$eF1i7R>1xFJREqN$MyN&?W_Xu11BgOwJ_He36zYFA8G~g>xAY%snp+=!maR$6wS_2Oo4yxC=o6hD>@1l z;m|CK3Y{yN0jYmK8e4G|WFMO3!5rrVgWh7O#>0QnjHas<^izDkdIo|Iwd7nTz1ZTa zmWQC5(>LQ5s~M^(y$<%=_FbM+=ZE=fAe&e)$TB>l>-^M7ap+yYl$3BJgHRd3i4|s@L?*n00=|MnMCxWK|pSyFue6 z<3rf`IQPp@pMl?QO9-g%Pv4F@Pi&{p#X((TxE;*H{4T~hF))nxNh^FuQsW8d7NR&>?(za}{pnk{3@)brtF}Ud#dJrn$FpFGt#ARJFywVL`$rcVmI9}3C zPvg7-JQ-yDl-VX%-;n%B$9sxRB3~*+M(Ai$#yAOXo&ziSb}g{ra;Jw;LFF8`s1*@K zSweh%3V=6XPg&iNQ(-|a()EV{mLSn9kv3Arep}kHs+IAJ$KTWYF4+R4AEt))2OI0t zU6jX4Wcb+HwS=93J4EylBols6O$M*AUB@P*(f$cB$0$S!VIh{ir5TLI0EVgg7;&_8AdC8Ge;^@H-!PB^6^Ul$%`<*N zZ|iGHeT6_$6AfKd_NE?_Y}SJ&20a zbTm;vxP4Hc2=v4-9tS02(vhm$V_}KM~%O>EC<>B6f0^Bf~IyL~- zIVX5QIqRkLbWg5E#>3cii=`-mFmciQ41XZo-t0+vqJ$$@TFkI(h914`8%YohSH5EL zJz9br1kn*xTjVltPY>vnozaxhhb*X&v&v`Tdf{M9)9Gk4#iH-&Fp4@R)6}N>*ANK~ zbaF#LmkEsYNYpG#rQ$S#pgw z>9x|Ko5z^F5)V=xyZ|KwhDIL(2M>n)A5{%8*7%dHT`dVhZawA&bFg+$GmUn zpK9DWwWU@+bEtG-xSh3Pb)0xb+qAe-acS0Mr(&skcQcX>sdk@|n3b2Hjt9Rd*>PXj zIeYgP{^D&b^+pJ^`~9;bUY);z9{C`!<4J1U+iTz^`>hm3nGv|g0=}JfKik%9GISFy zIRO3a63*pxN6kt4c7V8o1+~xk>p~=0B)4k@1NWg0WX|X~_z2FQXmNc7O96eWOW9^3 z9_7PgJ~jfZn~I{{lEswyghZEI&Id@t+c-3hzbt8lVvZ7A_-A<#6sPuHyRiGXkU6Bx zgA*AQ)Mc?})gEY%lz7~j4 z4h8F;2GoJBTa`ad|fJ;`umIH=ICg7?{MT zIxRK&5dluIFCszA@apBS!e@`d@j@JVCOu1`^5lZPR|@EmYG(I-x_%!9$IVrcOpdM( znrV||YRj#aOukTMGF;k9+!uMa;V~(HlyAHsjrKS`5>eU}3j*gcTv|Ebd^=nBn9%fo z(e&DKJT5&Nm|8VSAOICmCusOJw?rs8w28W!@+9}opYP+lX~*j7xr@MEhA(jko7EEu zl>yd*4sl2gY4WPB^-}ATrw@#NPP{@Xr_RIsx9i|*TsqB#;98a$8YH#VT_& ztL#9%dYt~XUA3@EFFe{%ZdhjXzH!?=-A|tzJGj2_OGXWI6X@NQ^t_t@TEflihA_p| z`PI5A(iB&?&iQLhca!KcpXtt8oz6EWL@+4Eqa&VC@gFUVd~nRvfu^1;UB3KX?=kb4 zcmWrl>S!BP;i}&|M*PuL2R{QVVwd~BYdOMIE>+USFOL=uRRL4&@+*5nASLnxKC$CA zP@#gx^q0_yrs$63kM0$li4Ki6KrxS0PQK%U<=^IwD;qMSa|}@*Ldwweh#MhK^htxt z!XW4Eqc<>LzE=OLd~A}u%xj8H8D|A^P`u`&#`MJrzFWO^3#;Z5_3)6fUe;U3#1*o# zxAr&OLhdenpV@SaPfdpW(x;eCSed8MN7K>4P8@~+IS)HRddDkOe zG@Z>+o8|%D@8Jo8C2iHK8d^D?THyu$n*FM}M>(5er$*BFo654pm7yu-OU0Jr&a!gv zo`9uDgR7<%)Q#V12?Mk!XkF+xol2#u?4x2k0S-?R_zV?42Y~g1f(3eX+F#XgzzR!P z*K%m~`%FCS@4A?sp!#O4oY#vB`6mMa4akHy>~9HtB~YoBaZoQ=J7*sL*(aAvFXnEM ztsVjd8YWAEc&zv&anUySIpyxrLB989dt>TyzSU)1 zRvRh0rY~WH@wJV(1ab6aCaj6}t#$ZY@f&V~v(4ssX1niq9wzp?j7z^iQ2jM2!4>S) z@gfg)px=-LOPn8>dp8bK?#(_uo&@^Be?Hm!d%8aW(Vs0GfQmf;i}Vix8nWEl0*I)z z=i@;CbpIUXW%CO~g3jmpe$5SbG=oFw0Za3a1vWw+Uy$r@Joaq4WHz`=rLA8A=ofE4 zHvRh0VO1l^3~aUB00aV`N0g7uT9lqWrW#M29q-S`IA=mk)Ksrfn-9M}pIFo%6OMIw z>!;MuJRf~*lFi*cdw$|ws`ZSXX=rQCOy}UbaQ1_&c@__&Rj$Hgp~(_$J)<2xF_5p+mrc zYJquFw{J#T<-T%z@X+9(tJ)}`eG^&)&V?Lz5Uf&XlF7ZdNYXVbQYjP3!R{?o;ZQuTsY|AZbPvTT` zL*>}*H&8OY?CJRlbR&T#>1QW`*~)l{tPxrJJQ@emU|=E1*h-)r(^xvK6k$$qOe}d^ zINdF6q+cZDT*^uXIg`(H(gbBW^EwKlj|cd$NstXTC7EfVi(r=f!#U{QD1jx@WJ#+5 zp$k!KxTPW~dXZ8oFQ8TWoGXzj&jsk)JW>ZCqR)g+3bn7c3?U#lPnMAUP+TyrXgN1Y zP+TX5+tE}7bEL`QN<~2@qSp9LBkoXA#H}p7a3)15LC;JWfMi`FK_#3NMIjAdM8K?t zKgZ`eV4v)dK8H(rg0%~TY@ewNrb3fNor;9_2(mfDd%2?am~pIuNvRzjuOS-bbXvuS zyj*GVGldx8!`tX&V#gt;O_nN%8+|5+CBmcOPgzjrjGt}mylvjQTYioCT=y#X-u7=v>Xnl-tMLLto#chDl+cMB0z zwpwWAFvb>~)Fb^CP!L;P3lSx@S}K$w%&yuuV794M@jUu;X#t|Du{??r@ufu)@#mx# zm!c^1=4+v*A!Pn+oic-?@?%k!fI}TWwnE;lRs4t{H&J-}Z#r9Sg$!Bhm{7rGj1b*+ z9F~9f8EPq!z}SCKo6oR_{s=1k4*XsK{IhPin$Dkq38lR)OJKu5Uh-^79*gZH7<%)E zkL5A-5l%o~8olUzXW<2&OG9{{S%=|?{}lDGMd|lhBpX|cHi(li%nBP0>>w|Axn80V z;R>6yOaWgKs!!4ZAR-`(ziSp@`%9+rFOu}%XUqOO0)_u?O45I0oBv}O%Jug$^e;Kf z|259c%+1R2KgrN^y+0Rp7-2Ubv=JCoCE{}TczzLCR~Qy7dNvR`L>a#><;)q-bfkL$ z^@+K*JkcLFGT)lSbsm{nuOOrf){mz&!f!mOVt3WHHhh^knmFwxZrZkt?<#Ye;){P- z&G_N#>DA_~OImk=r4=4iWlD^)W6iMSItF&S4*PlaZ24)W+dQ;4Wu&3w#sa?VqQf_u z)@Rja9v`n(p4My}*a>3l{v6RwMnR1;rrcPu<3Yf{9+>O1G z&V6A5Nb%v=$sj%HV7zB)wAKPPEqCj5*#*j#yP7mLFp?b%lDfnwJf#N7rv07Y7{sOQ z{Q0zbj~0bGa<(We+9vGh?5YbkXr@A|`YRdPNTx~-HlKSp6cF7#E6C?`!@>rXj(2Eo z#z8R|I2XcYw8}~jSG%mf|EO|u_$nWKLuBA6W+m>`wr#H-Rgy0~J@?C@*C@#8WN}J7 zGy3+`rESYt>cTg6s*lJ@wc`d-#M#-u_8|v!+(+<{ZDH(RlxO<@oIST-ju*23XhY7v zmdR*RmhIawAI_pxtH&pPM=pNQrqgAI)fLhyhdEJRJ4g2h&d!83)aT|u=o(Jw4=C<^ zz0|I-B+tA}jrLfb_Su2!k`|_zdUZOKa6AP`neMgt2>&>TpMF2_$@^+num~N@>#P9>P66wfpmj1O*V$eo+?oQst zcC9{V+p7o}GZsf^?e5a>35#h|>~7oc#95Z~W)^zj7x@7*5W4e3wOoq)h$S~8aD&SE zP!Q^sJ~$-Yyu7o&*}OMZ->@QWc4qs%g&w?jy`L}h zZ5DUT1K@JyX`l8;A&&W>ecX=>>z;gf4}117MD=(f(7im!gak%n8H9`E8#GKFG%OxO zNCg_|kaaM|U-!O;?biT@gany&$b-3;aR`|uWQb=G2dMFji#+DiwfO_Z5eXr%O|(vU zniWY2#FD-QQXo-5W}RrCy=lbRkKTc*5g8(|&9qMLz^-tXyabXVaY1ICX`kZ7#rm?( zA-V(t@yv^@S$NAiOm25oOrETEd!HC#9o4Hq1ic9b8w6jm5APc2&{OGQc1V6wc06G+ z@A9aT%|e&VLDSo;Avl&F43d$>0hPo9rEd9dMvevtWOLf5%sf%M zwn3i0PrK-&X6Q9h8vl^dOs$v-W`c0m-S9dHRG%H`KZZ2X^?eix`g74Je)10i?W@@*8aWBJ6EVq zJKi3Z9vVX?Yjp8kzlc+MA4bjg?SgjN}E9eogjmAjItuow`}~oLC*>rK_+V$1+D$@9NJiE zB&0375^9wut5uoG?($@wN5P-0xz$UDTs%NyVSiNDamCq_qLs+UhB^9&bwM>06)n!B z1-j(hWe>y>TA+^j5eDmMXm7@vVFn3SwZ&LbVsDF|$CLXnd>vVgQ9hE^z#tOEYK!Hf zG^NT~yqru+Esl#)j6!tWr^as!s`7@; z>J>(td_R?$8F+og8?+W|BvRgL@|`xC2v^&3zx9| zcat}N->LeqivOQob^pd6F>-SLyNrZNHGSKCHsn7$RRyg41GGqCOoJ0kJGusK<}RHP zez2H%KapKR4kx3ugk3gtP1+!QT$7Qh7ou8DSX&ZClP1{WhuCh(W7syo9U5@0N2D+? zY+XFfuX=N0uwx`Vh`zhoySIH(+C)k~3(KZrBV$ud!o;2PB>lkTK6yiEd6Uaf)bdi2 zJw8;`k}mH!Kiza--DFs%xjkDvR8uo>;>6O{`m+<>&IkKcnF^9CEe3fPe*~4472)pHKe-D@(O?`FPwV?BxjP>q#HbxF z$B?PxdT@{hbP@7?d?MRFPcsngWIqry5kvt2Rbyf!ih|&YyiI3anKvrTYASM4J-Lux zSLL(*zy?;Gc!Octb_incLY|#=(y|oFTJ3z%_F0ym$r2@31-puTrj7@e&%<2rM z6*WzzZd>_mbY~tpXG6X!$kFmzr%9dg)}>VV49p6ZIG4FQtwm0sHBEO zcDe?`QDd6D7_b%PAFtxf+3+Xw;xLYfW9CfC>lli5(-;Rqi*wDQ)Yoxk@senS;mROe z^;Ix~7_WRxRu(PXQJly$q9y|{(KOzaENi1;aIq)N)Yb5l9tX!w$1ydZN0=d;I`%0% zJsQWdJmg_8p$UEBcV6j|i$1mb0@BnvqBU5pfej5}`GWYoYteuWdw;EEi|h`QUT;Zn zc?C6aNuDLe*OS_HcRf&)@}-l7lZt5(HCM`nXx`+o02$@yV@`!@b-Ic(A7u;eH^yFs z>WbNWpzM&wG(djj{CDW`}Wg~OCR(vREEQ0k^3;Bi-0oy{&0#+p>+3tvLgr9G8hu*6o>X7-OBtHCU-RP+_xJknoYMjVYK3SsRv)c( zU8j$Ejq6_q?r&};Sm6Egl3Ug~nN56Mp({89Bd`gqJnHdcPVa+GfbHZC?Ot0|0MCWR zpid2D|mJ{GP)pee^U!GC)?V6I`%LJir7fYZt*sj zZXx}&$bbSNoCl7*2BMyhS&FiKEeTUbM7E%gTqLo5pdNo4oOyIyM&HcwZZLcE*pMLM z7a6*XYw;?&_n~Us$lWcDeAlwUN5d)++d54edo!MK7zoy+YYO5-RE`7VWntO`e!0q} zyiO66id{R{Jtm(nAf9KBy~crQ=>F8SaUYLr-!Ld|=;uWRpC^jFMue#^z{~dqnTw;y z1&T?X;`cI@XX0MN`bFq{mG{<0j-31KF=BG_x3dp9$&>&v z7^9DDXO2=e$hV517iX2m9NyCR!Ye>HPLk^k_byZUi-v03i>BwQhLs}mXttsy%2}9w zrC}Er$Qi@!H3vKB(5Y393UPnx|Ibg@Nqcb*0O=Oj-9F#j@0*)FLzF>aso{0HgX;`O z)Jb9wvrXGMWA2>i%uzSnK)Q@q6>d+1kr2vW+}c456msJ)pMcLvs$P}IZ!}v$3QD?@ zAK2WumMWE}5UJse+JhPN)D|LJ#*hDY%fxm}d!579qV11`yBd?8sSWw>+Pql4P@K5y z4L-N-dE>^d-R=B*(GS1181!TXfiK;_mv|cgaOm=oS;SF@}%g-r%Xd2E0% zNKb)7p|Oc6$AVn$3=s049^Aw}w|?32vaH(x09HZ0169D?((61XHF+^{5uJn`zmcDJ zm-J*s(o^6OcQyz`GtR*Un-Ar(+RnuGNt_Oqvh~hJ5?OA=3JjPpa`h=z8h{E+*Fv*)Zm z`j`dvP<=m)X*rarL$(C$1k9C0U^+ftsy^;{g7)s6Skmb8N;_KmPV~Dmo9<13O-;KZ z07hM0G_Nh#LkzjVIztharIv6|547`=mIx1+4m#?kx_OF8CHz_FNx||Er8)I^&(v9| zV~VszNDTGJj#$DCv1VYo@v4L)Z?96tSaQsXt;|E?ubxNB>P%+5qhL3XCd}Hnbg_m_ zOjE_Nc~DFY;9BFjgJx8YBttQfGpeh42uLky&m`glPYa=t93~h8)Quszt$naz%Z6=3 zxGaT36^+S8VWbfl;bfAt&K-?+CjGdZ{+bWrvFz}2VlaM4e%P}RY^z=9`ZZg&T?hXpp=#&L1(x~TQ*Sm>R$SKYw3Dcl&I!G(PjPUP42Dl)!?tYta zZ`&2Rk~m9g9A4QxPd~t5ATM_$giAPa@fHhFG3OA1wf$|@t${ZMfl~U7mtzwAgdB_v zWAMbEmQQ=~=YBQI!4NC==oX_jg=jkhK_ViEZ9#tN{Ln%R@Bc@7XjGP zzQSJ$O7&7ZXfPllSASV{_uUgoB(&^^$b|e=qSv7NbR0um&r6g*eKQxOsmh>B` z7L}w%3$vmgy21;)&?#h!x)4<9l1K`5rmbI}6}ico0TU?=kO}Q$lMZmlgsd=z!i0(? z0w?UxX}A^}JEfvPdk@46TCE`=X>!S5wL*n@@{cz&`x4Od)>F0&Qgj=E&B|;!C3Yi- zCK_Si#SA!=p`jmxa?-5ynK%iLit5P?x zLMj1b03^;vQ04(4U*@?~GHoXC z%r1|hjHR*MXEep@rpw!L5u#7J?8#1^Wc<#3>S zMKuggrWBp|0~!mE2L=E5F#qDn|LkG8Kmgbqz~T6j{!N7;;!SAKixSRyIazUi-K*|K z+M{Wtg}r7$!T>>ET+F>)!q)~m2d50bfsOlY*+5l#y$#;HRDly zicJ@`Y{a0!8-~RXOV9x4oi-cA#es+8^_T0xl#*j;P*A6xFq^$Qrv8TfI#^6uPy`E< zBBtnei@K>xy46iXUAz(@AxKPdP&so$Mb`&w6Gu$bmkI9lMTgt(JHi$MdOL7DT~V@| zU9}WdF2it)l5|k8C;k*&dCG!v8WHdHKR6OUYw43qu6>@}60_lW^$-zw_Tk7O-`E_v zxGZrp{E0Yjpx9#@Wq3LKJ!qH`MYHEb$z%u+;B{?@b%SGtAAe`Ub zDHO5i>QeK9tLVT6#F^Gc@5y$sT?>!X2@jPLxi5N*)MC1$TcWh6Z-8y>zSBNIe4m~d zf*2er_;KDEwWoMK@72&T{_%E6G;NS`9E)q|Y}6XblfpV^O|N#`_Cx05+reV-^JVo9 zS%Pw&dm}C@54YD5J8XAZ%Sf&ceUFw^)bXs8V;61Ih7VsWa;!xCvX2!k*pWEMw&2nR z=A&`#y48&;*L9Ca)em;kHjPBgUnntdRryno9EC?{Gde|9BHHC&&$pYaxgbktzb1h9 zj)964hH){!2X{vOiUi#o1}2-$Lul1|U^3W|<=LQOli|J|o(h3YM+t$D$&|FY)-za< zRR9pC5fZ`jyv*GCWTbeDrHRbHr)x zedSl40PWL4m_Fw%Eycnn?Nk}M4gP+MtwR>~I9Ft^p z44$2#skmy*k+OyR9Ci;&YK3Lc!ZMzh54m&40ZFTjIUep9;+A2+8L zE6}43GmX1xoPD*VEk<1aaMet*K2`@71<@)?9N9_YI@8pg4y_Z;FKm~Vw zIrb+_M-O}+R<9-g4b;jkl2CikmKmlU4*o{z%?!9!tw)-^nUx#1P`C^vd&f5HnUCDB z?=!4CPDFjO%bx`8+uw}WQhhR_WE(`+cq)m8WHs5b^n#gtQCMJ?;9{IZbItegA!hg? z+&qGbI(MNFAfBa0)tY)rY%oi4@XvDd3dkTDds=3{->^Zt(J-nqF%o+kx;FM%ct8K+ zZgt`i8~K~q`75=_`d>(G{&y2Qf6v+dSH=IK*kSt4`0skijcV8bvxnUC^CTp|>CeWa zn~>72t$c}g5l1t>+=tDO;)nnPnlR5gV~#CtTZE78m_(}xhm*SI$wMLj^@WM~n@P{i z*%u4m4hC||$D_#;uP%D>n8U=)=%K~WWgHhvx|Cj~L|n4@(pHE9^Gcjhf%XM`x8l(k>WQ~{4?E;xw<$0~Z_`T~TX6%l%6ulB6-ukrOES94-Ww&GNHmE9 zqd@Q^LD&BNcqRbd-G?TiegU5?+}DLnr#TFWE(r*HVen$Bd};%K?)q7fJej;sDiuPl zzkG@qZ;%Afn8t%2n5<=T_ub%){76LnHwSpH04}VdIQ-pr$_qmi868jyjQe7T#pguH z(=h{Wb?JU@i8JK110wK*?_cAS?%}Z;?{2aGOvqEG@K9KW4`NJRm1l614h0u`7QRJ| zFqyZqeOf)ngp}|s#G6J73GGncv5j}!_Qs9#o{SdJDk<7qYBw<%Rs5NdFZXp=ie=0q z#p@`aJG1iblr7+H<^&wVBr>as(%4yKgdaGD_v5uVZGLxxbqt}8 zA8tDSw*h%}+t*I}z;(~|1+`oJLB;RU)~D(C(6-PgM_O$-AmZ47=J_jS|~RT*6+Gad?ofd19CAgJh5UwOU*3IJbuQPsxarK8NWc0w*`*J$2Jp6Hzyd6N&j*g!(s0@La{<x^Z2CG0j#EQ3*{Ei_<+4lBh1^(p-8rtwle`$gpi09u#LFq zmE8)R{9JT=Q{;y|=QM?uZFC0-+KbRof%u?wBWnjd5xpK%V36gDNFZOD1S8f=gvz*P zSj$}72;f8qtNq$NY6QvW$47pB+97OL@s0jvx|yg!vPOi31ZJ1Z)K4ix`_DhA=_6Fy zet35Jz8o)-{q9>4g%tesU-G4Mghz_h=4^U6B;@wyJOKj}Jn&a_k78RRm0K)ygBe>Y zGGNTA9y9elYr(HI7z|9Jf5t;`z4qj{O#s;i=g;tmz_BiLpY0UzYcYR$ovE4jJyF?Y z<3JAfZ`3!_FNk8kUU2Dx21w&a4CZ^SQ5Rnq0nFBU#5v&Sq6-{wtPAMZ7VR5(V<9+W zv7yaS6qiIZ%oPDa^4weVpqe;n(CY$gF3QOw+!i{4K^?ki_e{_4Ja1K2v1g~J>kIS| zeh_~a4~uuiF{w9(ag})Sb=2hl=#x+-sI^8W)q*m|P~?hWn{UTZUPmy`0EOlgT#HE@ z%a3aGddHQtV6Ce1Sv;2Ah7J#eBeE8fG?|^^^ji`?aqt}ThFKmG>5qzaAqgU1!)9oTws`+C=75Qp5WFCxdsH;{cy_Y^u#0J3kr%#YqbMTk zAlEaPucj!98GkvG7ZSFU{?eV>V^9#`F5xCR{Gcc#->FuEnoMatl9Jz8Q_GAa=QSxL zEF*>$m<=Wj+9L<)@+|tgyDbgtpsOOr6`GCqZ`(T~?cN8CA83r@y@7B?)#nUv#%&DW zmuMV@ECh6+9>@oNqgo64uB0U#D+*EQaSINQ8mJohMY-lXGh!y!b(M%}q!36&wU#J9 zZidoz7Kuu#5Qs&!7AZe!#@BTe`R9O;=-)Oxt(dMibKQZ8qTFY4GLi!FA}ewEmrx3# znkROOuRn>Xpvs`Q6&vx0^ctN%-(yjQmjaK?)*R*a8nsK2%G4g#TGQ=tWw_xFRxV?? z6l0L6+~iTv^Uc2jM|k!tjjmaz+X2gT02!?m+2Nh)x8X&E>li8^3Zr<`n0G;fdd{&4 zL~q@MzXr2kI&#A2TL^BmR$b>3Nv`ZbY{#Oyrnt6gmTn4OqZc1{0mRFpyOtZb0)Ccm z<#aDU%?1#*M^k?MGj;!2F=g>L#q?LGkoCV1D*W%Jn3(_G9{SHgg)FSh|9fLOQq|XX zoelA0JLl(#pFg%i`Y-`;lk;GDqY!hh->k)eUn3^&AEaN6QUE)O5rFe5mTw+M*)9vVNs5(4 z5u7{@21eG4Ea6W|aK8<)v2Twc!Uq*c{;~}^nRu7`V#OcU)0Z~m;5wkp&T=hmNB=kx z7-A{io@bjtG&9PD1;h2RFiP_Sru-XvN*cZ&bPp8#%%Hh}IE11LzkZClS7)TV?MbUs zk%x4l6Kg}Y36*iTPM!}{mmFcyFFvduX|S2w4q2N4BbZ4i8-%&-`7ZXc=`VHIO` zrIHD1116IS{8q`}_+%OifNT%>&yLB&YNTLUJ{u{{kH&{^Qi4DTVvZJ!D-12e=VyB+ zJu(ti4=VfXN5=(_Zdh8Y@ZQsIlZfpH?N)bS!bb#3B!?~B@P@A(r+R0HC855;3QO@*;AUL9e zL~kUq-^krOoj=BiF2K%@#_}ym$~l9+jTC%Nkj%Wdkv_SPi7%-5jpM zaTA_o@k+wx%fQpQCV$DonU#yC@?=TwmC-&oTU9xDIe$=m#Z?ynS=7Z)yx}|IYpS;8 zr&w}InHde7jDPW5`OK7q?hAhYmd4}wD?4jD6e1BIBew6P$U)qDeI#YteLh`b-HIE$ zZGL-GYk!e_PvJHjpe-yb*C1u@f}IXiwQ7Czg^p*SUu#J5S{pHP#KIlWpj3-tAFye} zriqa;c{?@=*dVUvot)g;Ahj*`8MfA-SG_{*@`j?$*rumfMQD+{eNzdjLfM{gi1^sQ zO2B_Nyt-idf<0z1=I*u59J?Dg}I_*XB{k6#n?o-IQH25KF8pyYJ)R%MNu80Kx zeSS!r`@xo2Mv+_NeY62;`(BzDf)F3R_0Q_2mWLiwWNPLLnu4i2b3$N)-+tU(Y`fn3 zs5xbuw{6_Ixe%y?stB#i=PZU0WMF7{r|N2E{=f!Les5h%X})kn2wSR{gjaB_-j+!( z>^(ML(#cQy@(VtbmPLVpy*H`VZYuzE%^mDnJvb z0ch{pPqQ1{g~69r9TxOgHt>@vTr=I}8LJ7SIWC4z!8xz$UarohZC90dI2!Y}Uu)nt zm+9*|-iQNW2CE<~R!OTQ-gXywmW=96)ofqNhaC-&tYUt!1|O-g9^z>fp79EMlC^y?>=3EXl z?7O&)xI0QB^mml2L=$hVIFRE|pwJ&&CC? z4vz4_i(gG<(2v}X`N}cmkJIfBRo^0XMU+VPLc?BUsa$j4_XPqA5YxNwlM1kB`3z?ND^ z6nO%H6W6>ZQ`%Zb#&06s5*3}w(eve$Wi=xwPCS`v8>&%BL1?Om2Q#KD#r_c|Kt6C0 zQJRU7RW(?e54l8aU~O!BL}0wQs8Kt%_uc0%jojPZ`s)VG$FK2^5t=2yKEA#`1CQvt zZZThUX`NUU)phj!OwOleAqZ8}?5WiO(tWSnzRMrntRWdf3%cbX0prE+G%n^d)%|pd zz>z`ow{!%*aK3F_6seaVl?2sS1L`uwhCBN@!Zvz zyNN>>H{-e76R$WzmwU{Ov6f`J;NbE+4%`eI`A-0ptu-B!T53XmLR<65HG;ba;Mk1u zAp&Mz=#RZEto6rcmpdbR6MtjEP>IU+&VWh(19Cxhywlm+&+-~Lo}cuvVfn>Cf07Vd zSELC1?mTM1r#s?7aV)PRCC|%LU_*v!qb=P-C%+}U7hZLM5GQQC7k)#Z`N2lNCxk&O z|C>KTV=an^S%T7<*uZ6mv6&3E$Vac30HD*=1A*gt^%K0V#g5R5%~X0(!cnca`8%-6 zcF9E))x!{?o?(26_3SdU<_5##QChi<1L&B5CF#U**RXtuq6h$nZSa@-HO>Nx>c9t^AHjYj??K>u-v=ul|e_shsC4#6%D3g z6i-#G4{0s6ecWs%{OglLu}|MA9xwYJ6*^)kCi*ElrhZ4Yku+n-eB)2d^JEF>=;v`@ zz*s7bhzk;M`wr6S3P)nBt#(>0wPEC8sVRd)IySCoX=}cjMpUe&tCdE)F4+6F(!n|% zx(-5&oX(M`wyIsBGm6!)XQBXZEy+Ex8DkqF$Kh4p_mt45rYwsUc{+f?Uo);AyI}mu zTn6K?!e==1*ovQlM~=^UKx(YJicFnR!zNYw1HhhlQcNQ&nv&ROdoVb!5HxpfJS$+L5@LV5Wca(0B~JvAt}& zBwKqvh%PXTwk|Z9NSNF#@vOtF4HR-Y-;G+b9d`?0Q+<4&VNq4iLp5h*Uuey>cz3Y%1ljW*ylP1Zv=rT6#*%FlwU7 zV>OTFKi-xntv*pPeX80++OZdBbczih+t69r{L;1g_`_WAF1+x36>P4V+T_Ix5@8C= z9>{NYUV+pNRbiv#bAQe2+TXzfMC?ojy8P&>`|I@jz_V*<-AQ6(a{q&AdIZue%DGGe z#y^9ih;wlJTHN_*a!)G<*UfZ=w!{rqf8!igO43X<#r>|@FZJTMS4z+czRqeiVJrFQ z%`f9d+R2#`1FYQcxCm`^(;M{C2N1f%yDyFiiy%1o(M`h!ahHX#uo7pi(NsFky5!Fp zJi@cjL5cOGVd=2t*bzd+?6)h#A9j-4ABaZA8mJ|lgK!pS^%PT}ZT7Pjg=@0Cun1>$zG9Gn zB!Xis{e9!YpHwWPUr5-d^v%XEF(zvChYCe!??z*AqVbCJ$><2hY7`u()R0V4V)8qGH6@W znZ6+P%&VY;gE{F3h~ve|;4oM4$74GiRl)MRK$dlo#tI4{V7+3D`2#pA0wty_aA82eY$$JXo9A606TBle-S?!XXrcsUig-g6j1I5)!x>w;I;{ER3d z{WV2x)f<;c46w-}ykyw55!W#J!H}res|#l4^eQdIdS#O}D&m$9QC2 z2E%v_cMF@u{4zx_)jQ-Ec^ke%*tMjUGI|yFdbJz-dBf3ZrEb*oN#qx09VSg1CP$lG z%a0G)x};lPEqp@Jb%C<1_i+VE;MeR-8ka|Yn>S>DesjF<+*{VLSr)rXcDp-B?D9yc zP@^y+ulOu)Km=DS1cqZ5YGz&f$>Pq9r!9-Hv%?%jl|x#No53}^a6aBkh^W> z6ouskZgEl8Jtva;hy1Au$-n)Zy!|UO_-~6G|F0x}{=WX^UlsqylJ+|@>;K-``Ss7z zNl|p45}4NyUu*;!yjE zfui9ujAL4cw`fOG?rNmM3= zfOHa4qghovtYd5@=o_212tf!*XmQ3aRKdFv4Z{$GGIy%&JT92fav+%5FtnjOtZ-Fs zZA4n@!@<3dNJMl7iLKkU!Z zKkQEy3CnTnkmBO@0>ahI$Vys=qoqp^ACP}g3Y!bwAQM`+a%Qm$C3Lt9W9!TK&WG5y zEPF_4hOu($6uQFr=-E$R0kjRi5y3(q-Q-28LRs>a#>N<-`Vt8KTOTPI1VZNJbA5!8 zJ&A811k(*5wQ2R6Wipx6aYpuQb~~GP!U}E8I|;J`h()~8^}?9y?<*upvn69hitj(M z!6ehNSJi5N*_|_lkf0?!CG)~XP=-r=+bLXiU!r~0heEA78{Vi64+;`%v=cbTxJ{tZ zyTW&U>7AXe&wH5~tlkEYy#@%Y}{ z#*)6J!H*HcWH-VYsQ@sBEIXIhp-uPL*lYZ(b#HB1i$A#tQz^QX7Gck#>djZbR4ja> z%B5q%$0G@6lsMxo+`;$}xhQNW-tI*D{CpL1Z51fiyY}%rwAzG?^a%~0vahfz1kw&i~N+pT0KO84i|B|!{$UaL=Oxb=rOg-H~&6i`oVUZXAOGg~2V zbu``v2t!$7{psEMo3d z51c||8W8g(@&af&qu0GXq(IDwzN>htZ_TzDD7R%Bv~?Y%_AQn2{kS~E_V(J;E;zDe znHk+U@3>S;Iu^>fx`Wlt4we-?ieUO<3qd;xpx6W__wt9~!fK}OR?d?|&501fY1;df z$alQ$)eCTb%QSc0q|#*{WotT<_x&w?-D0wN$_(Hgi^cv>ytW1%E#dwhuIT?5^|P&|TV%%#dELlL-hS z#Q(kq-`RRz*saQo0n*2&mx$$NzC<2@;)o2=$D~J!?iRJk<>>m@CEH@Wwf`QF)oVt1 zLtL(MWK#`!{N=!vVI6KwdFNjD!z#%25DFL0oS{ z;Sft;scW`FTXWA|H3q9~^ZlU*lB$Jw144wCot_h_8_{Cyl^>RfZEPLPL6u-xA>5s|ij7W1$5JP@2*BHN^IKG;CgC-Dczl7)?EVp= zmB=Rg!Nr(r%de;T?RAe_v%{9z~f_F==_kQebh&lj_kyy4fl*XLv)?=gsg6y1U z5gNj;PNj!qov{XC*-O=+FvIP%c)}^)W0>`3YXx*tK-}PWx?5NALgTF02@TvJ@@$Ba z0KY+EmYZ>jFoDV=KB;9hU@sdJi$Z5;^aOzMTMvs-L%I_^VM&aEa*`qc2417OT*kbH zFoS(1#!JA=z$nd4oxF$}*elQrveOXzRG^=bn{iC0(*fmFw(?p$bebd&K_W6ArRF5C zJn9i1d5K_k8@4O;hnP1ph#}A~93cPj`QOv_v$iUcH#9oW66Gs}C22nxJTKH3HLBdI zhNK72#CvctWccVI^3V!a!ucOr z#e}RIU9k-Kupxla3=zZGp%xnVdzmFLtyCiaBsc-d&qkyD`7NaxC9B+uMDyAD&wmiy z)EFQMa^@W6gxh#t3`*okF(OyHL5mg2@?1MQJV_nJrt)f@3n}VGThM&HjqVuKFJsqy zL9L92@~(Tqt9eULVg?*V0h2XL?1pS%ZyR*OY}*|7bj)*5Na?qj^0=@co`;mLWYcez zEJjY6ZJ7`bG*3FAe<@ot>$2;s-9Yl7+}s+q2@p@FMjy#QDqgT;CDW(tv}1xTM8=qH zi4pd(UB8|g)T%K1ab z%&%H0S+(T3phW}+!tuZ64p7Ye>nOimltu=;vPVm+vGW350liH&xQd!DG(Dh@!Dght zDX71)(`^42v(tZ%#Q#^t{|N>4f8azZ*0%etNWG^CSEjXwuz39(x`ad`g4cX%>-}_;7Fu4_+ z0t9QNjVrxyH_3cuoE3|xR|`|E=84PZY0a0Y^!oiun1GXg=)ahd>9g(f;&l;aG(Nox z+Z5@|I0?umym#yMxu@ZKC(&@5E{ZqE)VWll-%_KA(ro4!$s0lvX8z=DWQ*de8cYf1 z&r<=(?&T5)ImDI-LVav4stT@iPUUrqc_FXt@ON?VLvTFHLAA>Vdjotw#+p;{Mk%!VM>wfG5{cGO>cad<8`cC=R=V2 zaSq_L#WT~7x7>DpG_IbhLahJB%C!|EyXu!#B{PfssbEM@s(G5zztT=;xQ z$OVxOpU1$-W8>i6FJyz)E<+dhth$ayX_Pn>q;*6khnP| zAlzK4U;S(;X<#@b$ET6#aj6OTV5?>GsByu`vz$xH{Y$IWNVQ7!Qg6d&>D>LH|8rz3 z+|}2thhv1xv7FII3zOUxjoS=VBcnD1ac+qRu-f0LtD-vV8gUsXpXrT}XBQf+A0$rA z0)8dqTy3XBw?63+C%A&4V|6Kdk%6HQtRkVI0?&g`TE2{Kn{iySxfG@puOC@TlnWD~ z*jNhN3LTox(3~L?7)cZeKR(k43tHl1EA)XL=i{~*IFG~C6}&a4T&}NyE}Jx4hMMWo z0H;z_k@f7a(h*`|7@}61$@lf~dKcp6{zFODGM@Nk;=1JirFgA*ze;T~TChr;XsAr+ zCoye(u1G!<*Tb9jN6p##FFbGv0sc7hX5d?2!bb(I+*ldr^%We+(AAj9P7O{iwZjqw z_8Q_>N=(E^2<*9bL;-PvmN;WxZHD3uC}SOILJl~VQs*Td`IE=lMbmXYgXq}^Xs}BE z;wComX~D7LbK|DVU!3}1RlP(kh?tP+`x+P@G2u%76((#C<2&|T_n&9q_kV9Q?k4H) zq3?ct;npPzfFaTWpJeB%>|T{1U1ERpB#7(xE)5;>M#T;l1#zO7t=~aJZ0)G&>heyl zKDMUn`pEJ|JPamcL$n4z>CR_FK%bi*SRRMD44@1WvLf_=nUN6@!lx}Y3~Dgxsy4Y| zFL^S4+h0JoXz;51fU7xXti^U9nRSi5;?_9u9#j|&HWzP1DqIhxzSEQ8nL0m`+&_0s zF8D64eRm3xfA=2$6(ZG#PqppAaI;4;D;;Y?PCZV)*qd8FkI~xmOIa{QfWIWsj9#1_ z!?<3m3DIoqv1K#E+wd{XMg~#en*j}Z65`aPz{#a-HS@{hSaKb)fXy*9O!%6=-IX%w z!C8alBNv{JwsW8!p*nxeS=>*UnZF&2k|j?N>bFWQh(UBEoYxKC z?6=q}a2V;4ujAzKyE#O8WWN-aouu!DZzDYp7W%@t`Y;!#pIuH^&+H-CxX+DdKUFL*CQ`*0}vdH`$CHA$jKgzR?7i%6W zU)OiB9h(!7D4*uUK@Q_Z+ZJAZS=%rKkq?qt+KT4-aAYGxcbz2=wdaIW6m!z~G!VfIPB>vp{%DkF zcCrw|?Q^gZ0*Ijz957m+nFup>6AxYJ4{G-s>YtP0Cd|~U+}V^BJT41h##cJh6$H@!lZQgWe~OJg+pNF zB8Zcvwkpzhnu^`wAUw8}{`@D?b8CyKV6o{H-P$a!N&rCFt`Fr~i;Iut$o+rRYL;}n zd!_H#m1j=eUPxm)VQVooJPh$QZN%l-2H?6GY363`WFB5m=8NDDfL%_j zGyXTl^v}fe-;~1t3h`k3zZk&&dsObfD*lfZ6XU;EOqpuhG3#vT-cL1zin*}?SZPo3 z3&U(UqzOiKRM>hmm~Tv%0iq<+ZkC$W7v?4|vtK%jjWUuX+9<4u!`KSP`0>ZtLX%tU zZ-@DmPZyIJt8X9OlmzA6F3cIrnvdrfXXQQy?CR*>^vZfUP^9xh6Ecgaqd1S!@P9wt zx_ot3tz2xY*sHNxvDwyCN4y;tuP~g$dA>cK?L)G)rOTNRbUUbCVVYX0*^oE9WHRgB z4+airQdsHKOWjM=zOW|`yXQkXypbH!CQ)V7P_WPX7E_lOGg%a4UKYDIW)>@SrGbdT z?v;i5$@pe{aqL3G;FO+V{4(PDx)vT(#GZNCBsDlf;1MN*_Z7b+K)sII!Ihhs>2*ig z6xJwZv$CyMs6M_IQ_ z(2QmBDXUceN~KzdkAxFn(4VuX9^R+~ojx3ET(Bd+-W@y;`mXX;JN5xTH-~AD>bvGY zzh{smcO98*U(Dy|gz}F|k$Gf>x(^k`+9#iiNW`%&8r^!_okk>2SqT@bJ^1L>cuJyC zi+*rQqD}b5zDa(lXl#+wpf9XTuY3Ofq2**y*Wc1-LxWlciXCg8Y?Pu zlqE7Hl8@|7Ja-0uD+)iTM#P9sD;;<+2iXKlRS|R*@p!D@h}7%K0}D+xeEWcFXPkIG zKP`$pvu*dZ!U&_RZt_!lI|#!2?G8h{*2pKtOKqZoKRf2vtb1W9q-XAvedcd2wh8x` z-DkrPBl7v3?`ls~w=_lkrF;%y70nA^@)Q zf-^FXpaQhT`+B1PjxnI&G3IfY{gSS1EJj=9JZ;pBQ3Iqg{jf}Ui4NF1!Tzijm9f~o z10JgtSi{ho1d~iVgZ%k!_g*Z@4zbcl2Y11Ex!9Vwp%605g8&qRb&>at!gC;px|%PN z&m8Wqg|G2RR3w!YK;GKdEQZIB>u%rmNz#elu{bDHXX&e}QQYCZ3Wn&I%OMLZVIUC= zi6w%k=$p-O%}YHr{;k~$qIg(>;Cs==cTOG2xgy2T1pl;Z*Kt>5&cc1$?=CIg<%g(6KWATqV(St5&M(Is~sq{uF=9wfVjZy(4Pu2 zIUbYMw7z44T$4i5!|FvWG|=7xZHgLp&O%LbYcemsnhs?;K!aqkKRwFcC(j;ZvGl`Hx|8t0)O>ULu=mstk6%!TrwKpCiAuW#C zv?_57(F;yU>{J1NE5poV#dmJ=6zZd3;N2?zjEbL?3{n|yllLyVJt&u_zc_s3$-j8*sSrFw|1+%DA_VBj8}teole9tx#x!HRk+p!g7^KCh8^p)7Pbz{ z*D!}Lg;&vp`ak2EeW~X2wm224v#Gz5_Us!r-fBwD4W2RDixy%d|wEJ$U4lIb+k5{aVKzXZShJ^}n8IY2Hh<=;ocsr@f2;C9ja{Jw} zQkGHbV3SJtQ)H%(*nQ6STL-Z45&`v}rolsgVl^55+O>L!1FW!{Ju~%(z1Gkl*;;;e~!QzW%243u~p<>ZQ zqx!Q58W_S3P+XkC`JxF0oTAk{!qu>mS_sOB{zNCeh?3y=ZDS!Oo(i@VQ7!51<{%cEpK#WJE{PA3rX?Lf7x|jkp zP1Rp%wHzWh$|aAVZ5k-t@=Vb1l1gB<7Q$)SVNtA(Pc9=jZM4@woLi?+rM*ZlHVPH4 zEpb-nkalX%ZAG_59s=pEB@pIb@)}8SbQEUcF=Oo`_H5qyE^aqTAt)m!)0_Vf?%pw0 zl;CUg-L`FepKaT=ZQJ(Qwr$(CZQHi3d)}El$vcz(&3wN}_3Enr)SXJIS9d+@_ehN@ zk9db+A+i$%We+9?B};8gFd+IGb=5i8?3JFd_wWbDIn^c5J4s}6P%*5Lw#NQcuneE{ zG#D$CBRImTBK^=<{EAY7X%!S3#lp&sy%T~?_x{DF<3hjg9vnjp$juF?qzMOza|Xym z_%69WF?iizIO6>rE(p5efr#>`Eh9M)TDT9O0L!e`|Uo#?)S)oORc9aG$`7n62 z;BC)v3!9Gz=74)yoja(Gv^l+a!|NE!5`OMw)`!y75IKr3c!XfYnJPql2)Xt!g;PRw zC^}>Ri^7#{6u=$PxO(+CTAS-@xq9_{#Z3cV7KWqf3?~5et?D2<#MKE=b&c?7X;3qR z>^v7SMkHc}NOAT$jv>H}sB8d&+eGvrKJNj7^zqFoDxU>G_CGEih|W6z1BD!zhUIZ1 zN)JMCnlHnnUUT_D(gUyxRd)WTtn5FEyjlN$Eb{)(06DDr)O-CGC>RhQVXK3rm>Fp+%XNVpj1|c_O z*A`0TI02FPg|Q!MCwj{wwI-1G5n@gv(gi1?p0DLsaQcv>pXvSbPTM zwWrhC&b4C!2~$}Lf%1Wnn0OA)6aT=6INR>9Vbw z59XON71^Djrg~nKJ|^aDLye?gb;UME@=1Hy?8a)|kn3NBn4|rwP$2;08obfaGLwIh zSg-TIGCy?L!@hqcv}9#zOf}KMi=N$)@S~PhbZ2P%H{tEPY%ccry!z z;R$rFy7t`3K+9{!;I=*`D-4#rv}>#{`rFWIUsdUyP;w~?q4OwxZ8D>Z07_k_BGp$& zRnjF&dJxdv;rGP*BrHyc_HLgyEP_|XkpPahn;q;sEIKlanu^(z{qvW(U%h0X@dV8` zwxO(TcvC4rL6r7YF1de0J#%U&^w{4|xx>-;qY@DWkT{XOJaASGlxW9ik0Shr&3{k* zZ1CPdO)l~#9YPqV=71WBVf@x=erUg`?q5CLpPF2{GZM^6b}p)Y4UK%O$Lid_@;y@RIpQ`OD6`(LHLUAcHZ+>bDT)e41n;- z!3>~R`{XP-C#QCQxw_FX1aI(K+@g2?oH}2>xx1QO=Y@g;c81pNRz|@eU)BHc$mKPn z0@=bDaH);>Wif-$%I??(`fCZT)g;fP&6}N+po0Nk2G_k(C9~}QrsC8^hvcUofY1CF zsHrO%qwYVfwQT=QRQ&%`H4}-#dBOj8aNZzONbAJR99(##Y0mmceb`Wu;E;DkL~Ik*FqCJKO5I~ zJH7pIN9DM*(zN$gsaMw-zUQr&ek^BB9@?)BR{wF*I(Uk5awB$I=e1dSQ)e z*^AWly5GMq5N6bf>9LU4cNjXq*#iavE=jC_Go>bh)|7%!cuhc=gJUdv1rtk8VP@b65@Er);QH8^VETn?Rkl?wn2pk6ba`Jd+XzhvS6_jd;y zD+4{<|73JB5HPVZGBW@7d0tBKP*zz*9qHwvA0tSlC>ZdO6G0J15y6pH#z7Qt$ca<1 z=7%6CKoU_-U?+%>V6GoxE|*b6w#kHG2v>>#~y6yAOb z{s$T+J?{XCx1JlDj}fuVHZ=hS50l@nNWFA+T!=Mw^{m9;h!S=Ems2v|0*cB8l7u02 z3U%`lt;xoS2gzC)8zu+TJaWm1$%)B}i0Lhn@R^^4g)XGg-Cx)Fp~$JH9fapR&$j+W zn^>(ZkI1@DHI7)#5c0Lz=yVnJ*MMG$zs+FRVwNE${UkRPTpRqQ+*lj~KsjzN{|uGI zN;BR^0l?aA&y0x{59r$(Q<((JZ#~@OFsEzK4*Bq3Z&A{TX2lb`ss@c)xoNCSvX?9h zrV